Updates referencesource to .NET 4.7
[mono.git] / mcs / class / referencesource / mscorlib / system / threading / Tasks / TaskExceptionHolder.cs
1 // ==++==
2 //
3 //   Copyright (c) Microsoft Corporation.  All rights reserved.
4 // 
5 // ==--==
6 // =+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
7 //
8 // TaskExceptionHolder.cs
9 //
10 // <OWNER>Microsoft</OWNER>
11 //
12 // An abstraction for holding and aggregating exceptions.
13 //
14 // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
15
16 // Disable the "reference to volatile field not treated as volatile" error.
17 #pragma warning disable 0420
18
19 namespace System.Threading.Tasks
20 {
21     using System;
22     using System.Collections.Generic;
23     using System.Collections.ObjectModel;
24     using System.Diagnostics.Contracts;
25     using System.Runtime.ExceptionServices;
26     using System.Security;
27
28     /// <summary>
29     /// An exception holder manages a list of exceptions for one particular task.
30     /// It offers the ability to aggregate, but more importantly, also offers intrinsic
31     /// support for propagating unhandled exceptions that are never observed. It does
32     /// this by aggregating and throwing if the holder is ever GC'd without the holder's
33     /// contents ever having been requested (e.g. by a Task.Wait, Task.get_Exception, etc).
34     /// This behavior is prominent in .NET 4 but is suppressed by default beyond that release.
35     /// </summary>
36     internal class TaskExceptionHolder
37     {
38         /// <summary>Whether we should propagate exceptions on the finalizer.</summary>
39         private readonly static bool s_failFastOnUnobservedException = ShouldFailFastOnUnobservedException();
40         /// <summary>Whether the AppDomain has started to unload.</summary>
41         private static volatile bool s_domainUnloadStarted;
42         /// <summary>An event handler used to notify of domain unload.</summary>
43         private static volatile EventHandler s_adUnloadEventHandler;
44
45         /// <summary>The task with which this holder is associated.</summary>
46         private readonly Task m_task;
47         /// <summary>
48         /// The lazily-initialized list of faulting exceptions.  Volatile
49         /// so that it may be read to determine whether any exceptions were stored.
50         /// </summary>
51         private volatile List<ExceptionDispatchInfo> m_faultExceptions;
52         /// <summary>An exception that triggered the task to cancel.</summary>
53         private ExceptionDispatchInfo m_cancellationException;
54         /// <summary>Whether the holder was "observed" and thus doesn't cause finalization behavior.</summary>
55         private volatile bool m_isHandled;
56
57         /// <summary>
58         /// Creates a new holder; it will be registered for finalization.
59         /// </summary>
60         /// <param name="task">The task this holder belongs to.</param>
61         internal TaskExceptionHolder(Task task)
62         {
63             Contract.Requires(task != null, "Expected a non-null task.");
64             m_task = task;
65             EnsureADUnloadCallbackRegistered();
66         }
67
68         [SecuritySafeCritical]
69         private static bool ShouldFailFastOnUnobservedException()
70         {
71             bool shouldFailFast = false;
72             #if !FEATURE_CORECLR
73             shouldFailFast = System.CLRConfig.CheckThrowUnobservedTaskExceptions();
74             #endif
75             return shouldFailFast;
76         }
77
78         private static void EnsureADUnloadCallbackRegistered()
79         {
80 #if MONO_FEATURE_MULTIPLE_APPDOMAINS
81             if (s_adUnloadEventHandler == null && 
82                 Interlocked.CompareExchange( ref s_adUnloadEventHandler,
83                                              AppDomainUnloadCallback, 
84                                              null) == null)
85             {
86                 AppDomain.CurrentDomain.DomainUnload += s_adUnloadEventHandler;
87             }
88 #endif
89         }
90
91         private static void AppDomainUnloadCallback(object sender, EventArgs e)
92         {
93             s_domainUnloadStarted = true;
94         }
95
96         /// <summary>
97         /// A finalizer that repropagates unhandled exceptions.
98         /// </summary>
99         ~TaskExceptionHolder()
100         {
101             // Raise unhandled exceptions only when we know that neither the process or nor the appdomain is being torn down.
102             // We need to do this filtering because all TaskExceptionHolders will be finalized during shutdown or unload
103             // regardles of reachability of the task (i.e. even if the user code was about to observe the task's exception),
104             // which can otherwise lead to spurious crashes during shutdown.
105             if (m_faultExceptions != null && !m_isHandled && 
106                 !Environment.HasShutdownStarted && !AppDomain.CurrentDomain.IsFinalizingForUnload() && !s_domainUnloadStarted)
107             {
108                 // We don't want to crash the finalizer thread if any ThreadAbortExceptions 
109                 // occur in the list or in any nested AggregateExceptions.  
110                 // (Don't rethrow ThreadAbortExceptions.)
111                 foreach (ExceptionDispatchInfo edi in m_faultExceptions)
112                 {
113                     var exp = edi.SourceException;
114                     AggregateException aggExp = exp as AggregateException;
115                     if (aggExp != null)
116                     {
117                         AggregateException flattenedAggExp = aggExp.Flatten();
118                         foreach (Exception innerExp in flattenedAggExp.InnerExceptions)
119                         {
120                             if (innerExp is ThreadAbortException)
121                                 return;
122                         }
123                     }
124                     else if (exp is ThreadAbortException)
125                     {
126                         return;
127                     }
128                 }
129
130                 // We will only propagate if this is truly unhandled. The reason this could
131                 // ever occur is somewhat subtle: if a Task's exceptions are observed in some
132                 // other finalizer, and the Task was finalized before the holder, the holder
133                 // will have been marked as handled before even getting here.
134
135                 // Give users a chance to keep this exception from crashing the process
136                 
137                 // First, publish the unobserved exception and allow users to observe it
138                 AggregateException exceptionToThrow = new AggregateException(
139                     Environment.GetResourceString("TaskExceptionHolder_UnhandledException"),
140                     m_faultExceptions);
141                 UnobservedTaskExceptionEventArgs ueea = new UnobservedTaskExceptionEventArgs(exceptionToThrow);
142                 TaskScheduler.PublishUnobservedTaskException(m_task, ueea);
143                 
144                 // Now, if we are still unobserved and we're configured to crash on unobserved, throw the exception.
145                 // We need to publish the event above even if we're not going to crash, hence
146                 // why this check doesn't come at the beginning of the method.
147                 if (s_failFastOnUnobservedException && !ueea.m_observed)
148                 {
149                     throw exceptionToThrow;
150                 }
151             }
152         }
153
154         /// <summary>Gets whether the exception holder is currently storing any exceptions for faults.</summary>
155         internal bool ContainsFaultList { get { return m_faultExceptions != null; } }
156
157         /// <summary>
158         /// Add an exception to the holder.  This will ensure the holder is
159         /// in the proper state (handled/unhandled) depending on the list's contents.
160         /// </summary>
161         /// <param name="exceptionObject">
162         /// An exception object (either an Exception, an ExceptionDispatchInfo,
163         /// an IEnumerable{Exception}, or an IEnumerable{ExceptionDispatchInfo}) 
164         /// to add to the list.
165         /// </param>
166         /// <remarks>
167         /// Must be called under lock.
168         /// </remarks>
169         internal void Add(object exceptionObject)
170         {
171             Add(exceptionObject, representsCancellation: false);
172         }
173
174         /// <summary>
175         /// Add an exception to the holder.  This will ensure the holder is
176         /// in the proper state (handled/unhandled) depending on the list's contents.
177         /// </summary>
178         /// <param name="representsCancellation">
179         /// Whether the exception represents a cancellation request (true) or a fault (false).
180         /// </param>
181         /// <param name="exceptionObject">
182         /// An exception object (either an Exception, an ExceptionDispatchInfo,
183         /// an IEnumerable{Exception}, or an IEnumerable{ExceptionDispatchInfo}) 
184         /// to add to the list.
185         /// </param>
186         /// <remarks>
187         /// Must be called under lock.
188         /// </remarks>
189         internal void Add(object exceptionObject, bool representsCancellation)
190         {
191             Contract.Requires(exceptionObject != null, "TaskExceptionHolder.Add(): Expected a non-null exceptionObject");
192             Contract.Requires(
193                 exceptionObject is Exception || exceptionObject is IEnumerable<Exception> || 
194                 exceptionObject is ExceptionDispatchInfo || exceptionObject is IEnumerable<ExceptionDispatchInfo>,
195                 "TaskExceptionHolder.Add(): Expected Exception, IEnumerable<Exception>, ExceptionDispatchInfo, or IEnumerable<ExceptionDispatchInfo>");
196
197             if (representsCancellation) SetCancellationException(exceptionObject);
198             else AddFaultException(exceptionObject);
199         }
200
201         /// <summary>Sets the cancellation exception.</summary>
202         /// <param name="exceptionObject">The cancellation exception.</param>
203         /// <remarks>
204         /// Must be called under lock.
205         /// </remarks>
206         private void SetCancellationException(object exceptionObject)
207         {
208             Contract.Requires(exceptionObject != null, "Expected exceptionObject to be non-null.");
209             
210             Contract.Assert(m_cancellationException == null, 
211                 "Expected SetCancellationException to be called only once.");
212                 // Breaking this assumption will overwrite a previously OCE,
213                 // and implies something may be wrong elsewhere, since there should only ever be one.
214
215             Contract.Assert(m_faultExceptions == null, 
216                 "Expected SetCancellationException to be called before any faults were added.");
217                 // Breaking this assumption shouldn't hurt anything here, but it implies something may be wrong elsewhere.
218                 // If this changes, make sure to only conditionally mark as handled below.
219
220             // Store the cancellation exception
221             var oce = exceptionObject as OperationCanceledException;
222             if (oce != null)
223             {
224                 m_cancellationException = ExceptionDispatchInfo.Capture(oce);
225             }
226             else
227             {
228                 var edi = exceptionObject as ExceptionDispatchInfo;
229                 Contract.Assert(edi != null && edi.SourceException is OperationCanceledException,
230                     "Expected an OCE or an EDI that contained an OCE");
231                 m_cancellationException = edi;
232             }
233
234             // This is just cancellation, and there are no faults, so mark the holder as handled.
235             MarkAsHandled(false);
236         }
237
238         /// <summary>Adds the exception to the fault list.</summary>
239         /// <param name="exceptionObject">The exception to store.</param>
240         /// <remarks>
241         /// Must be called under lock.
242         /// </remarks>
243         private void AddFaultException(object exceptionObject)
244         {
245             Contract.Requires(exceptionObject != null, "AddFaultException(): Expected a non-null exceptionObject");
246
247             // Initialize the exceptions list if necessary.  The list should be non-null iff it contains exceptions.
248             var exceptions = m_faultExceptions;
249             if (exceptions == null) m_faultExceptions = exceptions = new List<ExceptionDispatchInfo>(1);
250             else Contract.Assert(exceptions.Count > 0, "Expected existing exceptions list to have > 0 exceptions.");
251
252             // Handle Exception by capturing it into an ExceptionDispatchInfo and storing that
253             var exception = exceptionObject as Exception;
254             if (exception != null)
255             {
256                 exceptions.Add(ExceptionDispatchInfo.Capture(exception));
257             }
258             else
259             {
260                 // Handle ExceptionDispatchInfo by storing it into the list
261                 var edi = exceptionObject as ExceptionDispatchInfo;
262                 if (edi != null)
263                 {
264                     exceptions.Add(edi);
265                 }
266                 else
267                 {
268                     // Handle enumerables of exceptions by capturing each of the contained exceptions into an EDI and storing it
269                     var exColl = exceptionObject as IEnumerable<Exception>;
270                     if (exColl != null)
271                     {
272 #if DEBUG
273                         int numExceptions = 0;
274 #endif
275                         foreach (var exc in exColl)
276                         {
277 #if DEBUG
278                             Contract.Assert(exc != null, "No exceptions should be null");
279                             numExceptions++;
280 #endif
281                             exceptions.Add(ExceptionDispatchInfo.Capture(exc));
282                         }
283 #if DEBUG
284                         Contract.Assert(numExceptions > 0, "Collection should contain at least one exception.");
285 #endif
286                     }
287                     else
288                     {
289                         // Handle enumerables of EDIs by storing them directly
290                         var ediColl = exceptionObject as IEnumerable<ExceptionDispatchInfo>;
291                         if (ediColl != null)
292                         {
293                             exceptions.AddRange(ediColl);
294 #if DEBUG
295                             Contract.Assert(exceptions.Count > 0, "There should be at least one dispatch info.");
296                             foreach(var tmp in exceptions)
297                             {
298                                 Contract.Assert(tmp != null, "No dispatch infos should be null");
299                             }
300 #endif
301                         }
302                             // Anything else is a programming error
303                         else
304                         {
305                             throw new ArgumentException(Environment.GetResourceString("TaskExceptionHolder_UnknownExceptionType"), "exceptionObject");
306                         }
307                     }
308                 }
309             }
310                 
311
312             // If all of the exceptions are ThreadAbortExceptions and/or
313             // AppDomainUnloadExceptions, we do not want the finalization
314             // probe to propagate them, so we consider the holder to be
315             // handled.  If a subsequent exception comes in of a different
316             // kind, we will reactivate the holder.
317             for (int i = 0; i < exceptions.Count; i++)
318             {
319                 var t = exceptions[i].SourceException.GetType();
320                 if (t != typeof(ThreadAbortException) && t != typeof(AppDomainUnloadedException))
321                 {
322                     MarkAsUnhandled();
323                     break;
324                 }
325                 else if (i == exceptions.Count - 1)
326                 {
327                     MarkAsHandled(false);
328                 }
329             }
330         }
331
332         /// <summary>
333         /// A private helper method that ensures the holder is considered
334         /// unhandled, i.e. it is registered for finalization.
335         /// </summary>
336         private void MarkAsUnhandled()
337         {
338             // If a thread partially observed this thread's exceptions, we
339             // should revert back to "not handled" so that subsequent exceptions
340             // must also be seen. Otherwise, some could go missing. We also need
341             // to reregister for finalization.
342             if (m_isHandled)
343             {
344                 GC.ReRegisterForFinalize(this);
345                 m_isHandled = false;
346             }
347         }
348
349         /// <summary>
350         /// A private helper method that ensures the holder is considered
351         /// handled, i.e. it is not registered for finalization.
352         /// </summary>
353         /// <param name="calledFromFinalizer">Whether this is called from the finalizer thread.</param> 
354         internal void MarkAsHandled(bool calledFromFinalizer)
355         {
356             if (!m_isHandled)
357             {
358                 if (!calledFromFinalizer)
359                 {
360                     GC.SuppressFinalize(this);
361                 }
362
363                 m_isHandled = true;
364             }
365         }
366
367         /// <summary>
368         /// Allocates a new aggregate exception and adds the contents of the list to
369         /// it. By calling this method, the holder assumes exceptions to have been
370         /// "observed", such that the finalization check will be subsequently skipped.
371         /// </summary>
372         /// <param name="calledFromFinalizer">Whether this is being called from a finalizer.</param>
373         /// <param name="includeThisException">An extra exception to be included (optionally).</param>
374         /// <returns>The aggregate exception to throw.</returns>
375         internal AggregateException CreateExceptionObject(bool calledFromFinalizer, Exception includeThisException)
376         {
377             var exceptions = m_faultExceptions;
378             Contract.Assert(exceptions != null, "Expected an initialized list.");
379             Contract.Assert(exceptions.Count > 0, "Expected at least one exception.");
380
381             // Mark as handled and aggregate the exceptions.
382             MarkAsHandled(calledFromFinalizer);
383
384             // If we're only including the previously captured exceptions, 
385             // return them immediately in an aggregate.
386             if (includeThisException == null)
387                 return new AggregateException(exceptions);
388
389             // Otherwise, the caller wants a specific exception to be included, 
390             // so return an aggregate containing that exception and the rest.
391             Exception[] combinedExceptions = new Exception[exceptions.Count + 1];
392             for (int i = 0; i < combinedExceptions.Length - 1; i++)
393             {
394                 combinedExceptions[i] = exceptions[i].SourceException;
395             }
396             combinedExceptions[combinedExceptions.Length - 1] = includeThisException;
397             return new AggregateException(combinedExceptions);
398         }
399
400         /// <summary>
401         /// Wraps the exception dispatch infos into a new read-only collection. By calling this method, 
402         /// the holder assumes exceptions to have been "observed", such that the finalization 
403         /// check will be subsequently skipped.
404         /// </summary>
405         internal ReadOnlyCollection<ExceptionDispatchInfo> GetExceptionDispatchInfos()
406         {
407             var exceptions = m_faultExceptions;
408             Contract.Assert(exceptions != null, "Expected an initialized list.");
409             Contract.Assert(exceptions.Count > 0, "Expected at least one exception.");
410             MarkAsHandled(false);
411             return new ReadOnlyCollection<ExceptionDispatchInfo>(exceptions);
412         }
413
414         /// <summary>
415         /// Gets the ExceptionDispatchInfo representing the singular exception 
416         /// that was the cause of the task's cancellation.
417         /// </summary>
418         /// <returns>
419         /// The ExceptionDispatchInfo for the cancellation exception.  May be null.
420         /// </returns>
421         internal ExceptionDispatchInfo GetCancellationExceptionDispatchInfo()
422         {
423             var edi = m_cancellationException;
424             Contract.Assert(edi == null || edi.SourceException is OperationCanceledException,
425                 "Expected the EDI to be for an OperationCanceledException");
426             return edi;
427         }
428     }
429 }