src/DurableTask.Core/FailureDetails.cs (108 lines of code) (raw):

// ---------------------------------------------------------------------------------- // Copyright Microsoft Corporation // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ---------------------------------------------------------------------------------- #nullable enable namespace DurableTask.Core { using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using DurableTask.Core.Exceptions; using Newtonsoft.Json; // NOTE: This class is very similar to https://github.com/microsoft/durabletask-dotnet/blob/main/src/Abstractions/TaskFailureDetails.cs. // Any functional changes to this class should be mirrored in that class and vice versa. /// <summary> /// Details of an activity, orchestration, or entity operation failure. /// </summary> [Serializable] public class FailureDetails : IEquatable<FailureDetails> { /// <summary> /// Initializes a new instance of the <see cref="FailureDetails"/> class. /// </summary> /// <param name="errorType">The name of the error, which is expected to the the namespace-qualified name of the exception type.</param> /// <param name="errorMessage">The message associated with the error, which is expected to be the exception's <see cref="Exception.Message"/> property.</param> /// <param name="stackTrace">The exception stack trace.</param> /// <param name="innerFailure">The inner cause of the failure.</param> /// <param name="isNonRetriable">Whether the failure is non-retriable.</param> [JsonConstructor] public FailureDetails(string errorType, string errorMessage, string? stackTrace, FailureDetails? innerFailure, bool isNonRetriable) { this.ErrorType = errorType; this.ErrorMessage = errorMessage; this.StackTrace = stackTrace; this.InnerFailure = innerFailure; this.IsNonRetriable = isNonRetriable; } /// <summary> /// Initializes a new instance of the <see cref="FailureDetails"/> class from an exception object. /// </summary> /// <param name="e">The exception used to generate the failure details.</param> /// <param name="innerFailure">The inner cause of the failure.</param> public FailureDetails(Exception e, FailureDetails innerFailure) : this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, innerFailure, false) { } /// <summary> /// Initializes a new instance of the <see cref="FailureDetails"/> class from an exception object. /// </summary> /// <param name="e">The exception used to generate the failure details.</param> public FailureDetails(Exception e) : this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, FromException(e.InnerException), false) { } /// <summary> /// For testing purposes only: Initializes a new, empty instance of the <see cref="FailureDetails"/> class. /// </summary> public FailureDetails() { this.ErrorType = "None"; this.ErrorMessage = string.Empty; } /// <summary> /// Initializes a new instance of the <see cref="FailureDetails"/> class from a serialization context. /// </summary> protected FailureDetails(SerializationInfo info, StreamingContext context) { this.ErrorType = info.GetString(nameof(this.ErrorType)); this.ErrorMessage = info.GetString(nameof(this.ErrorMessage)); this.StackTrace = info.GetString(nameof(this.StackTrace)); this.InnerFailure = (FailureDetails)info.GetValue(nameof(this.InnerFailure), typeof(FailureDetails)); } /// <summary> /// Gets the type of the error, which is expected to the exception type's <see cref="Type.FullName"/> value. /// </summary> public string ErrorType { get; } /// <summary> /// Gets the message associated with the error, which is expected to be the exception's <see cref="Exception.Message"/> property. /// </summary> public string ErrorMessage { get; } /// <summary> /// Gets the exception stack trace. /// </summary> public string? StackTrace { get; } /// <summary> /// Gets the inner cause of this failure. /// </summary> public FailureDetails? InnerFailure { get; } /// <summary> /// Gets a value indicating whether this failure is non-retriable, meaning it should not be retried. /// </summary> public bool IsNonRetriable { get; } /// <summary> /// Gets a debug-friendly description of the failure information. /// </summary> public override string ToString() { return $"{this.ErrorType}: {this.ErrorMessage}"; } /// <summary> /// Returns <c>true</c> if the task failure was provided by the specified exception type. /// </summary> /// <remarks> /// This method allows checking if a task failed due to an exception of a specific type by attempting /// to load the type specified in <see cref="ErrorType"/>. If the exception type cannot be loaded /// for any reason, this method will return <c>false</c>. Base types are supported. /// </remarks> /// <typeparam name="T">The type of exception to test against.</typeparam> /// <returns>Returns <c>true</c> if the <see cref="ErrorType"/> value matches <typeparamref name="T"/>; <c>false</c> otherwise.</returns> public bool IsCausedBy<T>() where T : Exception { // This check works for .NET exception types defined in System.Core.PrivateLib (aka mscorelib.dll) Type? exceptionType = Type.GetType(this.ErrorType, throwOnError: false); // For exception types defined in the same assembly as the target exception type. exceptionType ??= typeof(T).Assembly.GetType(this.ErrorType, throwOnError: false); // For custom exception types defined in the app's assembly. exceptionType ??= Assembly.GetCallingAssembly().GetType(this.ErrorType, throwOnError: false); if (exceptionType == null) { // This last check works for exception types defined in any loaded assembly (e.g. NuGet packages, etc.). // This is a fallback that should rarely be needed except in obscure cases. List<Type> matchingExceptionTypes = AppDomain.CurrentDomain.GetAssemblies() .Select(a => a.GetType(this.ErrorType, throwOnError: false)) .Where(t => t is not null) .ToList(); if (matchingExceptionTypes.Count == 1) { exceptionType = matchingExceptionTypes[0]; } else if (matchingExceptionTypes.Count > 1) { throw new AmbiguousMatchException($"Multiple exception types with the name '{this.ErrorType}' were found."); } } return exceptionType != null && typeof(T).IsAssignableFrom(exceptionType); } /// <summary> /// Gets whether two <see cref="FailureDetails"/> objects are equivalent using value semantics. /// </summary> public override bool Equals(object other) => Equals(other as FailureDetails); /// <summary> /// Gets whether two <see cref="FailureDetails"/> objects are equivalent using value semantics. /// </summary> public bool Equals(FailureDetails? other) { if (ReferenceEquals(other, null)) { return false; } return this.ErrorType == other.ErrorType && this.ErrorMessage == other.ErrorMessage && this.StackTrace == other.StackTrace && this.InnerFailure == other.InnerFailure; } /// <inheritdoc/> public override int GetHashCode() { return (ErrorType, ErrorMessage, StackTrace, InnerFailure).GetHashCode(); } static string GetErrorMessage(Exception e) { if (e is TaskFailedException tfe) { return $"Task '{tfe.Name}' (#{tfe.ScheduleId}) failed with an unhandled exception: {tfe.Message}"; } else { return e.Message; } } static FailureDetails? FromException(Exception? e) { return e == null ? null : new FailureDetails(e); } } }