3 // Copyright (c) Microsoft Corporation. All rights reserved.
6 // =+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
8 // TaskExceptionHolder.cs
10 // <OWNER>Microsoft</OWNER>
12 // An abstraction for holding and aggregating exceptions.
14 // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
16 // Disable the "reference to volatile field not treated as volatile" error.
17 #pragma warning disable 0420
19 namespace System
.Threading
.Tasks
22 using System
.Collections
.Generic
;
23 using System
.Collections
.ObjectModel
;
24 using System
.Diagnostics
.Contracts
;
25 using System
.Runtime
.ExceptionServices
;
26 using System
.Security
;
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.
36 internal class TaskExceptionHolder
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
;
45 /// <summary>The task with which this holder is associated.</summary>
46 private readonly Task m_task
;
48 /// The lazily-initialized list of faulting exceptions. Volatile
49 /// so that it may be read to determine whether any exceptions were stored.
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
;
58 /// Creates a new holder; it will be registered for finalization.
60 /// <param name="task">The task this holder belongs to.</param>
61 internal TaskExceptionHolder(Task task
)
63 Contract
.Requires(task
!= null, "Expected a non-null task.");
65 EnsureADUnloadCallbackRegistered();
68 [SecuritySafeCritical
]
69 private static bool ShouldFailFastOnUnobservedException()
71 bool shouldFailFast
= false;
73 shouldFailFast
= System
.CLRConfig
.CheckThrowUnobservedTaskExceptions();
75 return shouldFailFast
;
78 private static void EnsureADUnloadCallbackRegistered()
80 #if MONO_FEATURE_MULTIPLE_APPDOMAINS
81 if (s_adUnloadEventHandler
== null &&
82 Interlocked
.CompareExchange( ref s_adUnloadEventHandler
,
83 AppDomainUnloadCallback
,
86 AppDomain
.CurrentDomain
.DomainUnload
+= s_adUnloadEventHandler
;
91 private static void AppDomainUnloadCallback(object sender
, EventArgs e
)
93 s_domainUnloadStarted
= true;
97 /// A finalizer that repropagates unhandled exceptions.
99 ~
TaskExceptionHolder()
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
)
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
)
113 var exp
= edi
.SourceException
;
114 AggregateException aggExp
= exp
as AggregateException
;
117 AggregateException flattenedAggExp
= aggExp
.Flatten();
118 foreach (Exception innerExp
in flattenedAggExp
.InnerExceptions
)
120 if (innerExp
is ThreadAbortException
)
124 else if (exp
is ThreadAbortException
)
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.
135 // Give users a chance to keep this exception from crashing the process
137 // First, publish the unobserved exception and allow users to observe it
138 AggregateException exceptionToThrow
= new AggregateException(
139 Environment
.GetResourceString("TaskExceptionHolder_UnhandledException"),
141 UnobservedTaskExceptionEventArgs ueea
= new UnobservedTaskExceptionEventArgs(exceptionToThrow
);
142 TaskScheduler
.PublishUnobservedTaskException(m_task
, ueea
);
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
)
149 throw exceptionToThrow
;
154 /// <summary>Gets whether the exception holder is currently storing any exceptions for faults.</summary>
155 internal bool ContainsFaultList { get { return m_faultExceptions != null; }
}
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.
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.
167 /// Must be called under lock.
169 internal void Add(object exceptionObject
)
171 Add(exceptionObject
, representsCancellation
: false);
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.
178 /// <param name="representsCancellation">
179 /// Whether the exception represents a cancellation request (true) or a fault (false).
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.
187 /// Must be called under lock.
189 internal void Add(object exceptionObject
, bool representsCancellation
)
191 Contract
.Requires(exceptionObject
!= null, "TaskExceptionHolder.Add(): Expected a non-null exceptionObject");
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>");
197 if (representsCancellation
) SetCancellationException(exceptionObject
);
198 else AddFaultException(exceptionObject
);
201 /// <summary>Sets the cancellation exception.</summary>
202 /// <param name="exceptionObject">The cancellation exception.</param>
204 /// Must be called under lock.
206 private void SetCancellationException(object exceptionObject
)
208 Contract
.Requires(exceptionObject
!= null, "Expected exceptionObject to be non-null.");
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.
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.
220 // Store the cancellation exception
221 var oce
= exceptionObject
as OperationCanceledException
;
224 m_cancellationException
= ExceptionDispatchInfo
.Capture(oce
);
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
;
234 // This is just cancellation, and there are no faults, so mark the holder as handled.
235 MarkAsHandled(false);
238 /// <summary>Adds the exception to the fault list.</summary>
239 /// <param name="exceptionObject">The exception to store.</param>
241 /// Must be called under lock.
243 private void AddFaultException(object exceptionObject
)
245 Contract
.Requires(exceptionObject
!= null, "AddFaultException(): Expected a non-null exceptionObject");
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.");
252 // Handle Exception by capturing it into an ExceptionDispatchInfo and storing that
253 var exception
= exceptionObject
as Exception
;
254 if (exception
!= null)
256 exceptions
.Add(ExceptionDispatchInfo
.Capture(exception
));
260 // Handle ExceptionDispatchInfo by storing it into the list
261 var edi
= exceptionObject
as ExceptionDispatchInfo
;
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
>;
273 int numExceptions
= 0;
275 foreach (var exc
in exColl
)
278 Contract
.Assert(exc
!= null, "No exceptions should be null");
281 exceptions
.Add(ExceptionDispatchInfo
.Capture(exc
));
284 Contract
.Assert(numExceptions
> 0, "Collection should contain at least one exception.");
289 // Handle enumerables of EDIs by storing them directly
290 var ediColl
= exceptionObject
as IEnumerable
<ExceptionDispatchInfo
>;
293 exceptions
.AddRange(ediColl
);
295 Contract
.Assert(exceptions
.Count
> 0, "There should be at least one dispatch info.");
296 foreach(var tmp
in exceptions
)
298 Contract
.Assert(tmp
!= null, "No dispatch infos should be null");
302 // Anything else is a programming error
305 throw new ArgumentException(Environment
.GetResourceString("TaskExceptionHolder_UnknownExceptionType"), "exceptionObject");
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
++)
319 var t
= exceptions
[i
].SourceException
.GetType();
320 if (t
!= typeof(ThreadAbortException
) && t
!= typeof(AppDomainUnloadedException
))
325 else if (i
== exceptions
.Count
- 1)
327 MarkAsHandled(false);
333 /// A private helper method that ensures the holder is considered
334 /// unhandled, i.e. it is registered for finalization.
336 private void MarkAsUnhandled()
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.
344 GC
.ReRegisterForFinalize(this);
350 /// A private helper method that ensures the holder is considered
351 /// handled, i.e. it is not registered for finalization.
353 /// <param name="calledFromFinalizer">Whether this is called from the finalizer thread.</param>
354 internal void MarkAsHandled(bool calledFromFinalizer
)
358 if (!calledFromFinalizer
)
360 GC
.SuppressFinalize(this);
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.
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
)
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.");
381 // Mark as handled and aggregate the exceptions.
382 MarkAsHandled(calledFromFinalizer
);
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
);
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
++)
394 combinedExceptions
[i
] = exceptions
[i
].SourceException
;
396 combinedExceptions
[combinedExceptions
.Length
- 1] = includeThisException
;
397 return new AggregateException(combinedExceptions
);
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.
405 internal ReadOnlyCollection
<ExceptionDispatchInfo
> GetExceptionDispatchInfos()
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
);
415 /// Gets the ExceptionDispatchInfo representing the singular exception
416 /// that was the cause of the task's cancellation.
419 /// The ExceptionDispatchInfo for the cancellation exception. May be null.
421 internal ExceptionDispatchInfo
GetCancellationExceptionDispatchInfo()
423 var edi
= m_cancellationException
;
424 Contract
.Assert(edi
== null || edi
.SourceException
is OperationCanceledException
,
425 "Expected the EDI to be for an OperationCanceledException");