diff --git a/src/Abstractions/SubOrchestrationFailedException.cs b/src/Abstractions/SubOrchestrationFailedException.cs new file mode 100644 index 000000000..7bc12d82a --- /dev/null +++ b/src/Abstractions/SubOrchestrationFailedException.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using CoreOrchestrationException = DurableTask.Core.Exceptions.OrchestrationException; + +namespace Microsoft.DurableTask; + +/// +/// Exception that gets thrown when a durable task sub-orchestration, fails with an +/// unhandled exception. +/// +/// +/// Detailed information associated with a particular task failure, including exception details, can be found in the +/// property. +/// +public sealed class SubOrchestrationFailedException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// The failed sub-orchestrationname. + /// The task ID. + /// The failure details. + public SubOrchestrationFailedException(string taskName, int taskId, TaskFailureDetails failureDetails) + : base(GetExceptionMessage(taskName, taskId, failureDetails, null)) + { + this.TaskName = taskName; + this.TaskId = taskId; + this.FailureDetails = failureDetails; + } + + /// + /// Gets the name of the failed sub-orchestration. + /// + public string TaskName { get; } + + /// + /// Gets the ID of the failed sub-orchestration. + /// + /// + /// 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. + /// + public int TaskId { get; } + + /// + /// Gets the details of the task failure, including exception information. + /// + 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}"; + } +} diff --git a/src/Abstractions/TaskFailedException.cs b/src/Abstractions/TaskFailedException.cs index 18e0addf3..8362c02b7 100644 --- a/src/Abstractions/TaskFailedException.cs +++ b/src/Abstractions/TaskFailedException.cs @@ -4,7 +4,7 @@ namespace Microsoft.DurableTask; /// -/// 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. /// /// diff --git a/src/Abstractions/TaskOrchestrationContext.cs b/src/Abstractions/TaskOrchestrationContext.cs index ecba913be..0c6fb58bc 100644 --- a/src/Abstractions/TaskOrchestrationContext.cs +++ b/src/Abstractions/TaskOrchestrationContext.cs @@ -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; @@ -332,9 +333,9 @@ public virtual Task CallSubOrchestratorAsync(TaskName orchestratorName, TaskOpti /// /// Thrown if the calling thread is anything other than the main orchestrator thread. /// - /// + /// /// The sub-orchestration failed with an unhandled exception. The details of the failure can be found in the - /// property. + /// property. /// public virtual Task CallSubOrchestratorAsync( TaskName orchestratorName, object? input = null, TaskOptions? options = null) diff --git a/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs b/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs index 8ce7d5125..bf0434216 100644 --- a/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs +++ b/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs @@ -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; @@ -197,8 +198,8 @@ public override async Task CallSubOrchestratorAsync( } 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!)); diff --git a/src/Worker/Core/Shims/TaskOrchestrationShim.cs b/src/Worker/Core/Shims/TaskOrchestrationShim.cs index d3fb7aa36..17daef523 100644 --- a/src/Worker/Core/Shims/TaskOrchestrationShim.cs +++ b/src/Worker/Core/Shims/TaskOrchestrationShim.cs @@ -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 diff --git a/test/Grpc.IntegrationTests/OrchestrationErrorHandling.cs b/test/Grpc.IntegrationTests/OrchestrationErrorHandling.cs index c8fb31181..0b2a44813 100644 --- a/test/Grpc.IntegrationTests/OrchestrationErrorHandling.cs +++ b/test/Grpc.IntegrationTests/OrchestrationErrorHandling.cs @@ -7,6 +7,7 @@ using Xunit.Abstractions; using Xunit.Sdk; + namespace Microsoft.DurableTask.Grpc.Tests; /// @@ -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()); + // The root orchestration failed due to a failure with the sub-orchestration, resulting in a Microsoft.DurableTask.SubOrchestrationFailedException + Assert.True(metadata.FailureDetails.IsCausedBy()); } [Theory] @@ -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()); + Assert.True(metadata.FailureDetails!.IsCausedBy()); } else { @@ -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()); + Assert.True(metadata.FailureDetails!.IsCausedBy()); } [Theory] @@ -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); @@ -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()); + Assert.True(metadata.FailureDetails!.IsCausedBy()); Assert.Contains("Sub", metadata.FailureDetails.ErrorMessage); Assert.NotNull(metadata.FailureDetails.InnerFailure); Assert.True(metadata.FailureDetails.InnerFailure!.IsCausedBy());