Skip to content
Closed
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
75 changes: 75 additions & 0 deletions src/Abstractions/SubOrchestrationFailedException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using CoreOrchestrationException = DurableTask.Core.Exceptions.OrchestrationException;

namespace Microsoft.DurableTask;

/// <summary>
/// Exception that gets thrown when a durable task sub-orchestration, fails with an
/// unhandled exception.
/// </summary>
/// <remarks>
/// Detailed information associated with a particular task failure, including exception details, can be found in the
/// <see cref="FailureDetails"/> property.
/// </remarks>
public sealed class SubOrchestrationFailedException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="SubOrchestrationFailedException"/> class.
/// </summary>
/// <param name="taskName">The failed sub-orchestrationname.</param>
/// <param name="taskId">The task ID.</param>
/// <param name="failureDetails">The failure details.</param>
public SubOrchestrationFailedException(string taskName, int taskId, TaskFailureDetails failureDetails)
: base(GetExceptionMessage(taskName, taskId, failureDetails, null))
{
this.TaskName = taskName;
this.TaskId = taskId;
this.FailureDetails = failureDetails;
}

/// <summary>
/// Gets the name of the failed sub-orchestration.
/// </summary>
public string TaskName { get; }

/// <summary>
/// Gets the ID of the failed sub-orchestration.
/// </summary>
/// <remarks>
/// Each durable task (activities, timers, sub-orchestrations, etc.) scheduled by a task orchestrator has an
/// auto-incrementing ID associated with it. This ID is used to distinguish tasks from one another, even if, for
/// example, they are tasks that call the same activity. This ID can therefore be used to more easily correlate a
/// specific task failure to a specific task.
/// </remarks>
public int TaskId { get; }

/// <summary>
/// Gets the details of the task failure, including exception information.
/// </summary>
public TaskFailureDetails FailureDetails { get; }

// This method is the same as the one in `TaskFailedException` to keep the exception message format consistent.
static string GetExceptionMessage(string taskName, int taskId, TaskFailureDetails? details, Exception? cause)
{
// NOTE: Some integration tests depend on the format of this exception message.
string? subMessage = null;
if (details is not null)
{
subMessage = details.ErrorMessage;
}
else if (cause is global::DurableTask.Core.Exceptions.OrchestrationException coreEx)
{
subMessage = coreEx.FailureDetails?.ErrorMessage;
}

if (subMessage is null)
{
subMessage = cause?.Message;
}

return subMessage is null
? $"Task '{taskName}' (#{taskId}) failed with an unhandled exception."
: $"Task '{taskName}' (#{taskId}) failed with an unhandled exception: {subMessage}";
}
}
2 changes: 1 addition & 1 deletion src/Abstractions/TaskFailedException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
namespace Microsoft.DurableTask;

/// <summary>
/// Exception that gets thrown when a durable task, such as an activity or a sub-orchestration, fails with an
/// Exception that gets thrown when a durable task, such as an activity , fails with an
/// unhandled exception.
/// </summary>
/// <remarks>
Expand Down
5 changes: 3 additions & 2 deletions src/Abstractions/TaskOrchestrationContext.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.DurableTask.Abstractions;
using Microsoft.DurableTask.Entities;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -332,9 +333,9 @@ public virtual Task CallSubOrchestratorAsync(TaskName orchestratorName, TaskOpti
/// <exception cref="InvalidOperationException">
/// Thrown if the calling thread is anything other than the main orchestrator thread.
/// </exception>
/// <exception cref="TaskFailedException">
/// <exception cref="SubOrchestrationFailedException">
/// The sub-orchestration failed with an unhandled exception. The details of the failure can be found in the
/// <see cref="TaskFailedException.FailureDetails"/> property.
/// <see cref="SubOrchestrationFailedException.FailureDetails"/> property.
/// </exception>
public virtual Task CallSubOrchestratorAsync(
TaskName orchestratorName, object? input = null, TaskOptions? options = null)
Expand Down
5 changes: 3 additions & 2 deletions src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using DurableTask.Core;
using DurableTask.Core.Entities.OperationFormat;
using DurableTask.Core.Serializing.Internal;
using Microsoft.DurableTask.Abstractions;
using Microsoft.DurableTask.Entities;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -197,8 +198,8 @@ public override async Task<TResult> CallSubOrchestratorAsync<TResult>(
}
catch (global::DurableTask.Core.Exceptions.SubOrchestrationFailedException e)
{
// Hide the core DTFx types and instead use our own
throw new TaskFailedException(
// Hide the core DTFx types and instead use our own SubOrchestrationFailedException
throw new SubOrchestrationFailedException(
orchestratorName,
e.ScheduleId,
TaskFailureDetails.FromCoreFailureDetails(e.FailureDetails!));
Expand Down
10 changes: 10 additions & 0 deletions src/Worker/Core/Shims/TaskOrchestrationShim.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ public TaskOrchestrationShim(
FailureDetails = new FailureDetails(e, e.FailureDetails.ToCoreFailureDetails()),
};
}
catch (SubOrchestrationFailedException e)
{
// Convert back to something the Durable Task Framework natively understands so that
// failure details are correctly propagated.
// This has to be DurableTask.Core.TaskFailedException instead of DurableTask.Core.SubOrchestratorFailedException because of core logic.
throw new CoreTaskFailedException(e.Message, e.InnerException)
{
FailureDetails = new FailureDetails(e, e.FailureDetails.ToCoreFailureDetails()),
};
}
finally
{
// if user code crashed inside a critical section, or did not exit it, do that now
Expand Down
17 changes: 9 additions & 8 deletions test/Grpc.IntegrationTests/OrchestrationErrorHandling.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Xunit.Abstractions;
using Xunit.Sdk;


namespace Microsoft.DurableTask.Grpc.Tests;

/// <summary>
Expand Down Expand Up @@ -327,8 +328,8 @@ public async Task RetrySubOrchestrationFailures(int expectedNumberOfAttempts, Ty
Assert.NotNull(metadata.FailureDetails);
Assert.Contains(errorMessage, metadata.FailureDetails!.ErrorMessage);

// The root orchestration failed due to a failure with the sub-orchestration, resulting in a TaskFailedException
Assert.True(metadata.FailureDetails.IsCausedBy<TaskFailedException>());
// The root orchestration failed due to a failure with the sub-orchestration, resulting in a Microsoft.DurableTask.SubOrchestrationFailedException
Assert.True(metadata.FailureDetails.IsCausedBy<SubOrchestrationFailedException>());
}

[Theory]
Expand Down Expand Up @@ -391,11 +392,11 @@ public async Task RetrySubOrchestratorFailuresCustomLogicAndPolicy(
//Assert.Equal(expectedNumberOfAttempts, retryHandlerCalls);
Assert.Equal(expectedNumberOfAttempts, actualNumberOfAttempts);

// The root orchestration failed due to a failure with the sub-orchestration, resulting in a TaskFailedException
// The root orchestration failed due to a failure with the sub-orchestration, resulting in a Microsoft.DurableTask.SubOrchestrationFailedException
if (expRuntimeStatus == OrchestrationRuntimeStatus.Failed)
{
Assert.NotNull(metadata.FailureDetails);
Assert.True(metadata.FailureDetails!.IsCausedBy<TaskFailedException>());
Assert.True(metadata.FailureDetails!.IsCausedBy<SubOrchestrationFailedException>());
}
else
{
Expand Down Expand Up @@ -463,9 +464,9 @@ public async Task RetrySubOrchestratorFailuresCustomLogic(int expectedNumberOfAt
Assert.Equal(expectedNumberOfAttempts, retryHandlerCalls);
Assert.Equal(expectedNumberOfAttempts, actualNumberOfAttempts);

// The root orchestration failed due to a failure with the sub-orchestration, resulting in a TaskFailedException
// The root orchestration failed due to a failure with the sub-orchestration, resulting in a SubOrchestrationFailedException
Assert.NotNull(metadata.FailureDetails);
Assert.True(metadata.FailureDetails!.IsCausedBy<TaskFailedException>());
Assert.True(metadata.FailureDetails!.IsCausedBy<SubOrchestrationFailedException>());
}

[Theory]
Expand Down Expand Up @@ -539,7 +540,7 @@ static void ValidateInnermostFailureDetailsChain(TaskFailureDetails? failureDeta
{
await ctx.CallSubOrchestratorAsync("Sub");
}
catch (TaskFailedException ex)
catch (SubOrchestrationFailedException ex)
{
// Outer failure represents the orchestration failure
Assert.NotNull(ex.FailureDetails);
Expand Down Expand Up @@ -580,7 +581,7 @@ static void ValidateInnermostFailureDetailsChain(TaskFailureDetails? failureDeta

// Check to make sure that the wrapper failure details exist as expected
Assert.NotNull(metadata.FailureDetails);
Assert.True(metadata.FailureDetails!.IsCausedBy<TaskFailedException>());
Assert.True(metadata.FailureDetails!.IsCausedBy<SubOrchestrationFailedException>());
Assert.Contains("Sub", metadata.FailureDetails.ErrorMessage);
Assert.NotNull(metadata.FailureDetails.InnerFailure);
Assert.True(metadata.FailureDetails.InnerFailure!.IsCausedBy<TaskFailedException>());
Expand Down
Loading