Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions Test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ namespace DurableTask.Core.Tests
{
using System;
using System.Diagnostics;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using DurableTask.Core.Exceptions;
using DurableTask.Emulator;
Expand Down Expand Up @@ -203,7 +204,10 @@ await context.ScheduleWithRetry<object>(
tfe.FailureDetails.ErrorMessage == "This is a test exception" &&
tfe.FailureDetails.StackTrace!.Contains(typeof(ThrowInvalidOperationException).Name) &&
tfe.FailureDetails.IsCausedBy<InvalidOperationException>() &&
tfe.FailureDetails.IsCausedBy<Exception>()) // check that base types work too
tfe.FailureDetails.IsCausedBy<Exception>() &&
tfe.FailureDetails.InnerFailure != null &&
tfe.FailureDetails.InnerFailure.IsCausedBy<CustomException>() &&
tfe.FailureDetails.InnerFailure.ErrorMessage == "And this is its custom inner exception")
{
// Stop retrying
return false;
Expand Down Expand Up @@ -235,15 +239,31 @@ class ThrowInvalidOperationException : TaskActivity<string, string>
{
protected override string Execute(TaskContext context, string input)
{
throw new InvalidOperationException("This is a test exception");
throw new InvalidOperationException("This is a test exception",
new CustomException("And this is its custom inner exception"));
}
}

class ThrowInvalidOperationExceptionAsync : AsyncTaskActivity<string, string>
{
protected override Task<string> ExecuteAsync(TaskContext context, string input)
{
throw new InvalidOperationException("This is a test exception");
throw new InvalidOperationException("This is a test exception",
new CustomException("And this is its custom inner exception"));
}
}

[Serializable]
class CustomException : Exception
{
public CustomException(string message)
: base(message)
{
}

protected CustomException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/DurableTask.Core/DurableTask.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
<!-- Version Info -->
<PropertyGroup>
<MajorVersion>2</MajorVersion>
<MinorVersion>16</MinorVersion>
<PatchVersion>2</PatchVersion>
<MinorVersion>17</MinorVersion>
<PatchVersion>0</PatchVersion>
<VersionPrefix>$(MajorVersion).$(MinorVersion).$(PatchVersion)</VersionPrefix>
<FileVersion>$(VersionPrefix).0</FileVersion>
<!-- FileVersionRevision is expected to be set by the CI. This is useful for distinguishing between multiple builds of the same version. -->
Expand Down
4 changes: 2 additions & 2 deletions src/DurableTask.Core/Exceptions/OrchestrationException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont
public int EventId { get; set; }

/// <summary>
/// Gets additional details about the failure. May be <c>null</c> if the failure details collection is not enabled.
/// Gets or sets additional details about the failure. May be <c>null</c> if the failure details collection is not enabled.
/// </summary>
public FailureDetails? FailureDetails { get; internal set; }
public FailureDetails? FailureDetails { get; set; }
}
}
42 changes: 42 additions & 0 deletions src/DurableTask.Core/FailureDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@
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>
Expand All @@ -42,6 +48,16 @@ public FailureDetails(string errorType, string errorMessage, string? stackTrace,
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>
Expand Down Expand Up @@ -116,7 +132,33 @@ public override string ToString()
/// <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);
}

Expand Down
6 changes: 6 additions & 0 deletions src/DurableTask.Core/TaskOrchestrationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,12 @@ public void FailOrchestration(Exception failure)
details = orchestrationFailureException.Details;
}
}
else if (failure is TaskFailedException taskFailedException &&
this.ErrorPropagationMode == ErrorPropagationMode.UseFailureDetails)
{
// Propagate the original FailureDetails
failureDetails = taskFailedException.FailureDetails;
}
else
{
if (this.ErrorPropagationMode == ErrorPropagationMode.UseFailureDetails)
Expand Down