summaryrefslogtreecommitdiff
path: root/src/mscorlib/src/System/Threading/Tasks/TaskExceptionHolder.cs
diff options
context:
space:
mode:
authorJiyoung Yun <jy910.yun@samsung.com>2016-11-23 19:09:09 +0900
committerJiyoung Yun <jy910.yun@samsung.com>2016-11-23 19:09:09 +0900
commit4b4aad7217d3292650e77eec2cf4c198ea9c3b4b (patch)
tree98110734c91668dfdbb126fcc0e15ddbd93738ca /src/mscorlib/src/System/Threading/Tasks/TaskExceptionHolder.cs
parentfa45f57ed55137c75ac870356a1b8f76c84b229c (diff)
downloadcoreclr-4b4aad7217d3292650e77eec2cf4c198ea9c3b4b.tar.gz
coreclr-4b4aad7217d3292650e77eec2cf4c198ea9c3b4b.tar.bz2
coreclr-4b4aad7217d3292650e77eec2cf4c198ea9c3b4b.zip
Imported Upstream version 1.1.0upstream/1.1.0
Diffstat (limited to 'src/mscorlib/src/System/Threading/Tasks/TaskExceptionHolder.cs')
-rw-r--r--src/mscorlib/src/System/Threading/Tasks/TaskExceptionHolder.cs424
1 files changed, 424 insertions, 0 deletions
diff --git a/src/mscorlib/src/System/Threading/Tasks/TaskExceptionHolder.cs b/src/mscorlib/src/System/Threading/Tasks/TaskExceptionHolder.cs
new file mode 100644
index 0000000000..198db8e15c
--- /dev/null
+++ b/src/mscorlib/src/System/Threading/Tasks/TaskExceptionHolder.cs
@@ -0,0 +1,424 @@
+// 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.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();
+ }
+
+ [SecuritySafeCritical]
+ private static bool ShouldFailFastOnUnobservedException()
+ {
+ bool shouldFailFast = false;
+ #if !FEATURE_CORECLR
+ shouldFailFast = System.CLRConfig.CheckThrowUnobservedTaskExceptions();
+ #endif
+ return shouldFailFast;
+ }
+
+ 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.");
+
+ Contract.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.
+
+ Contract.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;
+ Contract.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 Contract.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
+ Contract.Assert(exc != null, "No exceptions should be null");
+ numExceptions++;
+#endif
+ exceptions.Add(ExceptionDispatchInfo.Capture(exc));
+ }
+#if DEBUG
+ Contract.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
+ Contract.Assert(exceptions.Count > 0, "There should be at least one dispatch info.");
+ foreach(var tmp in exceptions)
+ {
+ Contract.Assert(tmp != null, "No dispatch infos should be null");
+ }
+#endif
+ }
+ // Anything else is a programming error
+ else
+ {
+ throw new ArgumentException(Environment.GetResourceString("TaskExceptionHolder_UnknownExceptionType"), "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;
+ Contract.Assert(exceptions != null, "Expected an initialized list.");
+ Contract.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;
+ Contract.Assert(exceptions != null, "Expected an initialized list.");
+ Contract.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;
+ Contract.Assert(edi == null || edi.SourceException is OperationCanceledException,
+ "Expected the EDI to be for an OperationCanceledException");
+ return edi;
+ }
+ }
+}