Fix IDE0025 (use expression body for properties)
[mono-project.git] / netcore / System.Private.CoreLib / shared / System / Threading / Tasks / TaskExceptionHolder.cs
blobf7edb3c6d92b3d75c1b6542549d5ae53ecba75ad
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 // =+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
6 //
7 //
8 //
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
20 /// <summary>
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).
27 /// </summary>
28 internal class TaskExceptionHolder
30 /// <summary>The task with which this holder is associated.</summary>
31 private readonly Task m_task;
32 /// <summary>
33 /// The lazily-initialized list of faulting exceptions. Volatile
34 /// so that it may be read to determine whether any exceptions were stored.
35 /// </summary>
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;
42 /// <summary>
43 /// Creates a new holder; it will be registered for finalization.
44 /// </summary>
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.");
49 m_task = task;
52 /// <summary>
53 /// A finalizer that repropagates unhandled exceptions.
54 /// </summary>
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,
67 m_faultExceptions);
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;
76 /// <summary>
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.
79 /// </summary>
80 /// <param name="representsCancellation">
81 /// Whether the exception represents a cancellation request (true) or a fault (false).
82 /// </param>
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.
87 /// </param>
88 /// <remarks>
89 /// Must be called under lock.
90 /// </remarks>
91 internal void Add(object exceptionObject, bool representsCancellation)
93 Debug.Assert(exceptionObject != null, "TaskExceptionHolder.Add(): Expected a non-null exceptionObject");
94 Debug.Assert(
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>
105 /// <remarks>
106 /// Must be called under lock.
107 /// </remarks>
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);
127 else
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>
141 /// <remarks>
142 /// Must be called under lock.
143 /// </remarks>
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));
158 else
160 // Handle ExceptionDispatchInfo by storing it into the list
161 if (exceptionObject is ExceptionDispatchInfo edi)
163 exceptions.Add(edi);
165 else
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)
170 #if DEBUG
171 int numExceptions = 0;
172 #endif
173 foreach (Exception exc in exColl)
175 #if DEBUG
176 Debug.Assert(exc != null, "No exceptions should be null");
177 numExceptions++;
178 #endif
179 exceptions.Add(ExceptionDispatchInfo.Capture(exc));
181 #if DEBUG
182 Debug.Assert(numExceptions > 0, "Collection should contain at least one exception.");
183 #endif
185 else
187 // Handle enumerables of EDIs by storing them directly
188 if (exceptionObject is IEnumerable<ExceptionDispatchInfo> ediColl)
190 exceptions.AddRange(ediColl);
191 #if DEBUG
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");
197 #endif
199 // Anything else is a programming error
200 else
202 throw new ArgumentException(SR.TaskExceptionHolder_UnknownExceptionType, nameof(exceptionObject));
208 if (exceptions.Count > 0)
209 MarkAsUnhandled();
212 /// <summary>
213 /// A private helper method that ensures the holder is considered
214 /// unhandled, i.e. it is registered for finalization.
215 /// </summary>
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.
222 if (m_isHandled)
224 GC.ReRegisterForFinalize(this);
225 m_isHandled = false;
229 /// <summary>
230 /// A private helper method that ensures the holder is considered
231 /// handled, i.e. it is not registered for finalization.
232 /// </summary>
233 /// <param name="calledFromFinalizer">Whether this is called from the finalizer thread.</param>
234 internal void MarkAsHandled(bool calledFromFinalizer)
236 if (!m_isHandled)
238 if (!calledFromFinalizer)
240 GC.SuppressFinalize(this);
243 m_isHandled = true;
247 /// <summary>
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.
251 /// </summary>
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);
280 /// <summary>
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.
284 /// </summary>
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);
294 /// <summary>
295 /// Gets the ExceptionDispatchInfo representing the singular exception
296 /// that was the cause of the task's cancellation.
297 /// </summary>
298 /// <returns>
299 /// The ExceptionDispatchInfo for the cancellation exception. May be null.
300 /// </returns>
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");
306 return edi;