summaryrefslogtreecommitdiff
path: root/src/mscorlib/src/System/Threading/Tasks/TaskExceptionHolder.cs
blob: 45817dab23a444095d07de0fe9d7d1e1c88b1d74 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

// =+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
//
//
//
// An abstraction for holding and aggregating exceptions.
//
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-

// Disable the "reference to volatile field not treated as volatile" error.
#pragma warning disable 0420

namespace System.Threading.Tasks
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Diagnostics;
    using System.Diagnostics.Contracts;
    using System.Runtime.ExceptionServices;
    using System.Security;

    /// <summary>
    /// An exception holder manages a list of exceptions for one particular task.
    /// It offers the ability to aggregate, but more importantly, also offers intrinsic
    /// support for propagating unhandled exceptions that are never observed. It does
    /// this by aggregating and throwing if the holder is ever GC'd without the holder's
    /// contents ever having been requested (e.g. by a Task.Wait, Task.get_Exception, etc).
    /// This behavior is prominent in .NET 4 but is suppressed by default beyond that release.
    /// </summary>
    internal class TaskExceptionHolder
    {
        /// <summary>Whether we should propagate exceptions on the finalizer.</summary>
        private readonly static bool s_failFastOnUnobservedException = ShouldFailFastOnUnobservedException();
        /// <summary>Whether the AppDomain has started to unload.</summary>
        private static volatile bool s_domainUnloadStarted;
        /// <summary>An event handler used to notify of domain unload.</summary>
        private static volatile EventHandler s_adUnloadEventHandler;

        /// <summary>The task with which this holder is associated.</summary>
        private readonly Task m_task;
        /// <summary>
        /// The lazily-initialized list of faulting exceptions.  Volatile
        /// so that it may be read to determine whether any exceptions were stored.
        /// </summary>
        private volatile List<ExceptionDispatchInfo> m_faultExceptions;
        /// <summary>An exception that triggered the task to cancel.</summary>
        private ExceptionDispatchInfo m_cancellationException;
        /// <summary>Whether the holder was "observed" and thus doesn't cause finalization behavior.</summary>
        private volatile bool m_isHandled;

        /// <summary>
        /// Creates a new holder; it will be registered for finalization.
        /// </summary>
        /// <param name="task">The task this holder belongs to.</param>
        internal TaskExceptionHolder(Task task)
        {
            Contract.Requires(task != null, "Expected a non-null task.");
            m_task = task;
            EnsureADUnloadCallbackRegistered();
        }

        private static bool ShouldFailFastOnUnobservedException()
        {
            return false;
        }

        private static void EnsureADUnloadCallbackRegistered()
        {
            if (s_adUnloadEventHandler == null && 
                Interlocked.CompareExchange( ref s_adUnloadEventHandler,
                                             AppDomainUnloadCallback, 
                                             null) == null)
            {
                AppDomain.CurrentDomain.DomainUnload += s_adUnloadEventHandler;
            }
        }

        private static void AppDomainUnloadCallback(object sender, EventArgs e)
        {
            s_domainUnloadStarted = true;
        }

        /// <summary>
        /// A finalizer that repropagates unhandled exceptions.
        /// </summary>
        ~TaskExceptionHolder()
        {
            // Raise unhandled exceptions only when we know that neither the process or nor the appdomain is being torn down.
            // We need to do this filtering because all TaskExceptionHolders will be finalized during shutdown or unload
            // regardles of reachability of the task (i.e. even if the user code was about to observe the task's exception),
            // which can otherwise lead to spurious crashes during shutdown.
            if (m_faultExceptions != null && !m_isHandled && 
                !Environment.HasShutdownStarted && !AppDomain.CurrentDomain.IsFinalizingForUnload() && !s_domainUnloadStarted)
            {
                // We don't want to crash the finalizer thread if any ThreadAbortExceptions 
                // occur in the list or in any nested AggregateExceptions.  
                // (Don't rethrow ThreadAbortExceptions.)
                foreach (ExceptionDispatchInfo edi in m_faultExceptions)
                {
                    var exp = edi.SourceException;
                    AggregateException aggExp = exp as AggregateException;
                    if (aggExp != null)
                    {
                        AggregateException flattenedAggExp = aggExp.Flatten();
                        foreach (Exception innerExp in flattenedAggExp.InnerExceptions)
                        {
                            if (innerExp is ThreadAbortException)
                                return;
                        }
                    }
                    else if (exp is ThreadAbortException)
                    {
                        return;
                    }
                }

                // We will only propagate if this is truly unhandled. The reason this could
                // ever occur is somewhat subtle: if a Task's exceptions are observed in some
                // other finalizer, and the Task was finalized before the holder, the holder
                // will have been marked as handled before even getting here.

                // Give users a chance to keep this exception from crashing the process
                
                // First, publish the unobserved exception and allow users to observe it
                AggregateException exceptionToThrow = new AggregateException(
                    Environment.GetResourceString("TaskExceptionHolder_UnhandledException"),
                    m_faultExceptions);
                UnobservedTaskExceptionEventArgs ueea = new UnobservedTaskExceptionEventArgs(exceptionToThrow);
                TaskScheduler.PublishUnobservedTaskException(m_task, ueea);
                
                // Now, if we are still unobserved and we're configured to crash on unobserved, throw the exception.
                // We need to publish the event above even if we're not going to crash, hence
                // why this check doesn't come at the beginning of the method.
                if (s_failFastOnUnobservedException && !ueea.m_observed)
                {
                    throw exceptionToThrow;
                }
            }
        }

        /// <summary>Gets whether the exception holder is currently storing any exceptions for faults.</summary>
        internal bool ContainsFaultList { get { return m_faultExceptions != null; } }

        /// <summary>
        /// Add an exception to the holder.  This will ensure the holder is
        /// in the proper state (handled/unhandled) depending on the list's contents.
        /// </summary>
        /// <param name="exceptionObject">
        /// An exception object (either an Exception, an ExceptionDispatchInfo,
        /// an IEnumerable{Exception}, or an IEnumerable{ExceptionDispatchInfo}) 
        /// to add to the list.
        /// </param>
        /// <remarks>
        /// Must be called under lock.
        /// </remarks>
        internal void Add(object exceptionObject)
        {
            Add(exceptionObject, representsCancellation: false);
        }

        /// <summary>
        /// Add an exception to the holder.  This will ensure the holder is
        /// in the proper state (handled/unhandled) depending on the list's contents.
        /// </summary>
        /// <param name="representsCancellation">
        /// Whether the exception represents a cancellation request (true) or a fault (false).
        /// </param>
        /// <param name="exceptionObject">
        /// An exception object (either an Exception, an ExceptionDispatchInfo,
        /// an IEnumerable{Exception}, or an IEnumerable{ExceptionDispatchInfo}) 
        /// to add to the list.
        /// </param>
        /// <remarks>
        /// Must be called under lock.
        /// </remarks>
        internal void Add(object exceptionObject, bool representsCancellation)
        {
            Contract.Requires(exceptionObject != null, "TaskExceptionHolder.Add(): Expected a non-null exceptionObject");
            Contract.Requires(
                exceptionObject is Exception || exceptionObject is IEnumerable<Exception> || 
                exceptionObject is ExceptionDispatchInfo || exceptionObject is IEnumerable<ExceptionDispatchInfo>,
                "TaskExceptionHolder.Add(): Expected Exception, IEnumerable<Exception>, ExceptionDispatchInfo, or IEnumerable<ExceptionDispatchInfo>");

            if (representsCancellation) SetCancellationException(exceptionObject);
            else AddFaultException(exceptionObject);
        }

        /// <summary>Sets the cancellation exception.</summary>
        /// <param name="exceptionObject">The cancellation exception.</param>
        /// <remarks>
        /// Must be called under lock.
        /// </remarks>
        private void SetCancellationException(object exceptionObject)
        {
            Contract.Requires(exceptionObject != null, "Expected exceptionObject to be non-null.");
            
            Debug.Assert(m_cancellationException == null, 
                "Expected SetCancellationException to be called only once.");
                // Breaking this assumption will overwrite a previously OCE,
                // and implies something may be wrong elsewhere, since there should only ever be one.

            Debug.Assert(m_faultExceptions == null, 
                "Expected SetCancellationException to be called before any faults were added.");
                // Breaking this assumption shouldn't hurt anything here, but it implies something may be wrong elsewhere.
                // If this changes, make sure to only conditionally mark as handled below.

            // Store the cancellation exception
            var oce = exceptionObject as OperationCanceledException;
            if (oce != null)
            {
                m_cancellationException = ExceptionDispatchInfo.Capture(oce);
            }
            else
            {
                var edi = exceptionObject as ExceptionDispatchInfo;
                Debug.Assert(edi != null && edi.SourceException is OperationCanceledException,
                    "Expected an OCE or an EDI that contained an OCE");
                m_cancellationException = edi;
            }

            // This is just cancellation, and there are no faults, so mark the holder as handled.
            MarkAsHandled(false);
        }

        /// <summary>Adds the exception to the fault list.</summary>
        /// <param name="exceptionObject">The exception to store.</param>
        /// <remarks>
        /// Must be called under lock.
        /// </remarks>
        private void AddFaultException(object exceptionObject)
        {
            Contract.Requires(exceptionObject != null, "AddFaultException(): Expected a non-null exceptionObject");

            // Initialize the exceptions list if necessary.  The list should be non-null iff it contains exceptions.
            var exceptions = m_faultExceptions;
            if (exceptions == null) m_faultExceptions = exceptions = new List<ExceptionDispatchInfo>(1);
            else Debug.Assert(exceptions.Count > 0, "Expected existing exceptions list to have > 0 exceptions.");

            // Handle Exception by capturing it into an ExceptionDispatchInfo and storing that
            var exception = exceptionObject as Exception;
            if (exception != null)
            {
                exceptions.Add(ExceptionDispatchInfo.Capture(exception));
            }
            else
            {
                // Handle ExceptionDispatchInfo by storing it into the list
                var edi = exceptionObject as ExceptionDispatchInfo;
                if (edi != null)
                {
                    exceptions.Add(edi);
                }
                else
                {
                    // Handle enumerables of exceptions by capturing each of the contained exceptions into an EDI and storing it
                    var exColl = exceptionObject as IEnumerable<Exception>;
                    if (exColl != null)
                    {
#if DEBUG
                        int numExceptions = 0;
#endif
                        foreach (var exc in exColl)
                        {
#if DEBUG
                            Debug.Assert(exc != null, "No exceptions should be null");
                            numExceptions++;
#endif
                            exceptions.Add(ExceptionDispatchInfo.Capture(exc));
                        }
#if DEBUG
                        Debug.Assert(numExceptions > 0, "Collection should contain at least one exception.");
#endif
                    }
                    else
                    {
                        // Handle enumerables of EDIs by storing them directly
                        var ediColl = exceptionObject as IEnumerable<ExceptionDispatchInfo>;
                        if (ediColl != null)
                        {
                            exceptions.AddRange(ediColl);
#if DEBUG
                            Debug.Assert(exceptions.Count > 0, "There should be at least one dispatch info.");
                            foreach(var tmp in exceptions)
                            {
                                Debug.Assert(tmp != null, "No dispatch infos should be null");
                            }
#endif
                        }
                            // Anything else is a programming error
                        else
                        {
                            throw new ArgumentException(Environment.GetResourceString("TaskExceptionHolder_UnknownExceptionType"), nameof(exceptionObject));
                        }
                    }
                }
            }
                

            // If all of the exceptions are ThreadAbortExceptions and/or
            // AppDomainUnloadExceptions, we do not want the finalization
            // probe to propagate them, so we consider the holder to be
            // handled.  If a subsequent exception comes in of a different
            // kind, we will reactivate the holder.
            for (int i = 0; i < exceptions.Count; i++)
            {
                var t = exceptions[i].SourceException.GetType();
                if (t != typeof(ThreadAbortException) && t != typeof(AppDomainUnloadedException))
                {
                    MarkAsUnhandled();
                    break;
                }
                else if (i == exceptions.Count - 1)
                {
                    MarkAsHandled(false);
                }
            }
        }

        /// <summary>
        /// A private helper method that ensures the holder is considered
        /// unhandled, i.e. it is registered for finalization.
        /// </summary>
        private void MarkAsUnhandled()
        {
            // If a thread partially observed this thread's exceptions, we
            // should revert back to "not handled" so that subsequent exceptions
            // must also be seen. Otherwise, some could go missing. We also need
            // to reregister for finalization.
            if (m_isHandled)
            {
                GC.ReRegisterForFinalize(this);
                m_isHandled = false;
            }
        }

        /// <summary>
        /// A private helper method that ensures the holder is considered
        /// handled, i.e. it is not registered for finalization.
        /// </summary>
        /// <param name="calledFromFinalizer">Whether this is called from the finalizer thread.</param> 
        internal void MarkAsHandled(bool calledFromFinalizer)
        {
            if (!m_isHandled)
            {
                if (!calledFromFinalizer)
                {
                    GC.SuppressFinalize(this);
                }

                m_isHandled = true;
            }
        }

        /// <summary>
        /// Allocates a new aggregate exception and adds the contents of the list to
        /// it. By calling this method, the holder assumes exceptions to have been
        /// "observed", such that the finalization check will be subsequently skipped.
        /// </summary>
        /// <param name="calledFromFinalizer">Whether this is being called from a finalizer.</param>
        /// <param name="includeThisException">An extra exception to be included (optionally).</param>
        /// <returns>The aggregate exception to throw.</returns>
        internal AggregateException CreateExceptionObject(bool calledFromFinalizer, Exception includeThisException)
        {
            var exceptions = m_faultExceptions;
            Debug.Assert(exceptions != null, "Expected an initialized list.");
            Debug.Assert(exceptions.Count > 0, "Expected at least one exception.");

            // Mark as handled and aggregate the exceptions.
            MarkAsHandled(calledFromFinalizer);

            // If we're only including the previously captured exceptions, 
            // return them immediately in an aggregate.
            if (includeThisException == null)
                return new AggregateException(exceptions);

            // Otherwise, the caller wants a specific exception to be included, 
            // so return an aggregate containing that exception and the rest.
            Exception[] combinedExceptions = new Exception[exceptions.Count + 1];
            for (int i = 0; i < combinedExceptions.Length - 1; i++)
            {
                combinedExceptions[i] = exceptions[i].SourceException;
            }
            combinedExceptions[combinedExceptions.Length - 1] = includeThisException;
            return new AggregateException(combinedExceptions);
        }

        /// <summary>
        /// Wraps the exception dispatch infos into a new read-only collection. By calling this method, 
        /// the holder assumes exceptions to have been "observed", such that the finalization 
        /// check will be subsequently skipped.
        /// </summary>
        internal ReadOnlyCollection<ExceptionDispatchInfo> GetExceptionDispatchInfos()
        {
            var exceptions = m_faultExceptions;
            Debug.Assert(exceptions != null, "Expected an initialized list.");
            Debug.Assert(exceptions.Count > 0, "Expected at least one exception.");
            MarkAsHandled(false);
            return new ReadOnlyCollection<ExceptionDispatchInfo>(exceptions);
        }

        /// <summary>
        /// Gets the ExceptionDispatchInfo representing the singular exception 
        /// that was the cause of the task's cancellation.
        /// </summary>
        /// <returns>
        /// The ExceptionDispatchInfo for the cancellation exception.  May be null.
        /// </returns>
        internal ExceptionDispatchInfo GetCancellationExceptionDispatchInfo()
        {
            var edi = m_cancellationException;
            Debug.Assert(edi == null || edi.SourceException is OperationCanceledException,
                "Expected the EDI to be for an OperationCanceledException");
            return edi;
        }
    }
}