1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
5 // =+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
9 // An abstraction for holding and aggregating exceptions.
11 // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
13 using System
.Collections
.Generic
;
14 using System
.Collections
.ObjectModel
;
15 using System
.Diagnostics
;
16 using System
.Runtime
.ExceptionServices
;
18 namespace System
.Threading
.Tasks
21 /// An exception holder manages a list of exceptions for one particular task.
22 /// It offers the ability to aggregate, but more importantly, also offers intrinsic
23 /// support for propagating unhandled exceptions that are never observed. It does
24 /// this by aggregating and calling UnobservedTaskException event event if the holder
25 /// is ever GC'd without the holder's contents ever having been requested
26 /// (e.g. by a Task.Wait, Task.get_Exception, etc).
28 internal class TaskExceptionHolder
30 /// <summary>The task with which this holder is associated.</summary>
31 private readonly Task m_task
;
33 /// The lazily-initialized list of faulting exceptions. Volatile
34 /// so that it may be read to determine whether any exceptions were stored.
36 private volatile List
<ExceptionDispatchInfo
>? m_faultExceptions
;
37 /// <summary>An exception that triggered the task to cancel.</summary>
38 private ExceptionDispatchInfo
? m_cancellationException
;
39 /// <summary>Whether the holder was "observed" and thus doesn't cause finalization behavior.</summary>
40 private volatile bool m_isHandled
;
43 /// Creates a new holder; it will be registered for finalization.
45 /// <param name="task">The task this holder belongs to.</param>
46 internal TaskExceptionHolder(Task task
)
48 Debug
.Assert(task
!= null, "Expected a non-null task.");
53 /// A finalizer that repropagates unhandled exceptions.
55 ~
TaskExceptionHolder()
57 if (m_faultExceptions
!= null && !m_isHandled
)
59 // We will only propagate if this is truly unhandled. The reason this could
60 // ever occur is somewhat subtle: if a Task's exceptions are observed in some
61 // other finalizer, and the Task was finalized before the holder, the holder
62 // will have been marked as handled before even getting here.
64 // Publish the unobserved exception and allow users to observe it
65 AggregateException exceptionToThrow
= new AggregateException(
66 SR
.TaskExceptionHolder_UnhandledException
,
68 UnobservedTaskExceptionEventArgs ueea
= new UnobservedTaskExceptionEventArgs(exceptionToThrow
);
69 TaskScheduler
.PublishUnobservedTaskException(m_task
, ueea
);
73 /// <summary>Gets whether the exception holder is currently storing any exceptions for faults.</summary>
74 internal bool ContainsFaultList
=> m_faultExceptions
!= null;
77 /// Add an exception to the holder. This will ensure the holder is
78 /// in the proper state (handled/unhandled) depending on the list's contents.
80 /// <param name="representsCancellation">
81 /// Whether the exception represents a cancellation request (true) or a fault (false).
83 /// <param name="exceptionObject">
84 /// An exception object (either an Exception, an ExceptionDispatchInfo,
85 /// an IEnumerable{Exception}, or an IEnumerable{ExceptionDispatchInfo})
86 /// to add to the list.
89 /// Must be called under lock.
91 internal void Add(object exceptionObject
, bool representsCancellation
)
93 Debug
.Assert(exceptionObject
!= null, "TaskExceptionHolder.Add(): Expected a non-null exceptionObject");
95 exceptionObject
is Exception
|| exceptionObject
is IEnumerable
<Exception
> ||
96 exceptionObject
is ExceptionDispatchInfo
|| exceptionObject
is IEnumerable
<ExceptionDispatchInfo
>,
97 "TaskExceptionHolder.Add(): Expected Exception, IEnumerable<Exception>, ExceptionDispatchInfo, or IEnumerable<ExceptionDispatchInfo>");
99 if (representsCancellation
) SetCancellationException(exceptionObject
);
100 else AddFaultException(exceptionObject
);
103 /// <summary>Sets the cancellation exception.</summary>
104 /// <param name="exceptionObject">The cancellation exception.</param>
106 /// Must be called under lock.
108 private void SetCancellationException(object exceptionObject
)
110 Debug
.Assert(exceptionObject
!= null, "Expected exceptionObject to be non-null.");
112 Debug
.Assert(m_cancellationException
== null,
113 "Expected SetCancellationException to be called only once.");
114 // Breaking this assumption will overwrite a previously OCE,
115 // and implies something may be wrong elsewhere, since there should only ever be one.
117 Debug
.Assert(m_faultExceptions
== null,
118 "Expected SetCancellationException to be called before any faults were added.");
119 // Breaking this assumption shouldn't hurt anything here, but it implies something may be wrong elsewhere.
120 // If this changes, make sure to only conditionally mark as handled below.
122 // Store the cancellation exception
123 if (exceptionObject
is OperationCanceledException oce
)
125 m_cancellationException
= ExceptionDispatchInfo
.Capture(oce
);
129 var edi
= exceptionObject
as ExceptionDispatchInfo
;
130 Debug
.Assert(edi
!= null && edi
.SourceException
is OperationCanceledException
,
131 "Expected an OCE or an EDI that contained an OCE");
132 m_cancellationException
= edi
;
135 // This is just cancellation, and there are no faults, so mark the holder as handled.
136 MarkAsHandled(false);
139 /// <summary>Adds the exception to the fault list.</summary>
140 /// <param name="exceptionObject">The exception to store.</param>
142 /// Must be called under lock.
144 private void AddFaultException(object exceptionObject
)
146 Debug
.Assert(exceptionObject
!= null, "AddFaultException(): Expected a non-null exceptionObject");
148 // Initialize the exceptions list if necessary. The list should be non-null iff it contains exceptions.
149 List
<ExceptionDispatchInfo
>? exceptions
= m_faultExceptions
;
150 if (exceptions
== null) m_faultExceptions
= exceptions
= new List
<ExceptionDispatchInfo
>(1);
151 else Debug
.Assert(exceptions
.Count
> 0, "Expected existing exceptions list to have > 0 exceptions.");
153 // Handle Exception by capturing it into an ExceptionDispatchInfo and storing that
154 if (exceptionObject
is Exception exception
)
156 exceptions
.Add(ExceptionDispatchInfo
.Capture(exception
));
160 // Handle ExceptionDispatchInfo by storing it into the list
161 if (exceptionObject
is ExceptionDispatchInfo edi
)
167 // Handle enumerables of exceptions by capturing each of the contained exceptions into an EDI and storing it
168 if (exceptionObject
is IEnumerable
<Exception
> exColl
)
171 int numExceptions
= 0;
173 foreach (Exception exc
in exColl
)
176 Debug
.Assert(exc
!= null, "No exceptions should be null");
179 exceptions
.Add(ExceptionDispatchInfo
.Capture(exc
));
182 Debug
.Assert(numExceptions
> 0, "Collection should contain at least one exception.");
187 // Handle enumerables of EDIs by storing them directly
188 if (exceptionObject
is IEnumerable
<ExceptionDispatchInfo
> ediColl
)
190 exceptions
.AddRange(ediColl
);
192 Debug
.Assert(exceptions
.Count
> 0, "There should be at least one dispatch info.");
193 foreach (ExceptionDispatchInfo tmp
in exceptions
)
195 Debug
.Assert(tmp
!= null, "No dispatch infos should be null");
199 // Anything else is a programming error
202 throw new ArgumentException(SR
.TaskExceptionHolder_UnknownExceptionType
, nameof(exceptionObject
));
208 if (exceptions
.Count
> 0)
213 /// A private helper method that ensures the holder is considered
214 /// unhandled, i.e. it is registered for finalization.
216 private void MarkAsUnhandled()
218 // If a thread partially observed this thread's exceptions, we
219 // should revert back to "not handled" so that subsequent exceptions
220 // must also be seen. Otherwise, some could go missing. We also need
221 // to reregister for finalization.
224 GC
.ReRegisterForFinalize(this);
230 /// A private helper method that ensures the holder is considered
231 /// handled, i.e. it is not registered for finalization.
233 /// <param name="calledFromFinalizer">Whether this is called from the finalizer thread.</param>
234 internal void MarkAsHandled(bool calledFromFinalizer
)
238 if (!calledFromFinalizer
)
240 GC
.SuppressFinalize(this);
248 /// Allocates a new aggregate exception and adds the contents of the list to
249 /// it. By calling this method, the holder assumes exceptions to have been
250 /// "observed", such that the finalization check will be subsequently skipped.
252 /// <param name="calledFromFinalizer">Whether this is being called from a finalizer.</param>
253 /// <param name="includeThisException">An extra exception to be included (optionally).</param>
254 /// <returns>The aggregate exception to throw.</returns>
255 internal AggregateException
CreateExceptionObject(bool calledFromFinalizer
, Exception
? includeThisException
)
257 List
<ExceptionDispatchInfo
>? exceptions
= m_faultExceptions
;
258 Debug
.Assert(exceptions
!= null, "Expected an initialized list.");
259 Debug
.Assert(exceptions
.Count
> 0, "Expected at least one exception.");
261 // Mark as handled and aggregate the exceptions.
262 MarkAsHandled(calledFromFinalizer
);
264 // If we're only including the previously captured exceptions,
265 // return them immediately in an aggregate.
266 if (includeThisException
== null)
267 return new AggregateException(exceptions
);
269 // Otherwise, the caller wants a specific exception to be included,
270 // so return an aggregate containing that exception and the rest.
271 Exception
[] combinedExceptions
= new Exception
[exceptions
.Count
+ 1];
272 for (int i
= 0; i
< combinedExceptions
.Length
- 1; i
++)
274 combinedExceptions
[i
] = exceptions
[i
].SourceException
;
276 combinedExceptions
[^
1] = includeThisException
;
277 return new AggregateException(combinedExceptions
);
281 /// Wraps the exception dispatch infos into a new read-only collection. By calling this method,
282 /// the holder assumes exceptions to have been "observed", such that the finalization
283 /// check will be subsequently skipped.
285 internal ReadOnlyCollection
<ExceptionDispatchInfo
> GetExceptionDispatchInfos()
287 List
<ExceptionDispatchInfo
>? exceptions
= m_faultExceptions
;
288 Debug
.Assert(exceptions
!= null, "Expected an initialized list.");
289 Debug
.Assert(exceptions
.Count
> 0, "Expected at least one exception.");
290 MarkAsHandled(false);
291 return new ReadOnlyCollection
<ExceptionDispatchInfo
>(exceptions
);
295 /// Gets the ExceptionDispatchInfo representing the singular exception
296 /// that was the cause of the task's cancellation.
299 /// The ExceptionDispatchInfo for the cancellation exception. May be null.
301 internal ExceptionDispatchInfo
? GetCancellationExceptionDispatchInfo()
303 ExceptionDispatchInfo
? edi
= m_cancellationException
;
304 Debug
.Assert(edi
== null || edi
.SourceException
is OperationCanceledException
,
305 "Expected the EDI to be for an OperationCanceledException");