diff --git a/release_notes.md b/release_notes.md index 6738d3ab6..6f5e0c462 100644 --- a/release_notes.md +++ b/release_notes.md @@ -1,6 +1,6 @@ # Release Notes -## Microsoft.Azure.Functions.Worker.Extensions.DurableTask v1.1.0-preview.2 +## Microsoft.Azure.Functions.Worker.Extensions.DurableTask ### New Features @@ -10,20 +10,15 @@ ### Dependency Updates -`Microsoft.DurableTask.*` to `1.1.0-preview.2` - -## Microsoft.Azure.WebJobs.Extensions.DurableTask v2.12.0-preview.1 +## Microsoft.Azure.WebJobs.Extensions.DurableTask ### New Features -- Updates to take advantage of new core-entity support - ### Bug Fixes +- Fix failed orchestration/entities not showing up as function invocation failures. + ### Breaking Changes ### Dependency Updates -`Microsoft.Azure.DurableTask.Core` to `2.16.0-preview.2` -`Microsoft.Azure.DurableTask.AzureStorage` to `1.16.0-preview.2` - diff --git a/src/WebJobs.Extensions.DurableTask/ContextImplementations/RemoteEntityContext.cs b/src/WebJobs.Extensions.DurableTask/ContextImplementations/RemoteEntityContext.cs index 07ea0e921..c2bf96369 100644 --- a/src/WebJobs.Extensions.DurableTask/ContextImplementations/RemoteEntityContext.cs +++ b/src/WebJobs.Extensions.DurableTask/ContextImplementations/RemoteEntityContext.cs @@ -3,12 +3,9 @@ #nullable enable using System; using System.Collections.Generic; -using DurableTask.Core; -using DurableTask.Core.Command; +using DurableTask.Core.Entities; using DurableTask.Core.Entities.OperationFormat; -using DurableTask.Core.History; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Microsoft.Azure.WebJobs.Extensions.DurableTask { @@ -20,9 +17,40 @@ public RemoteEntityContext(EntityBatchRequest batchRequest) } [JsonProperty("request")] - public EntityBatchRequest Request { get; private set; } + internal EntityBatchRequest Request { get; private set; } [JsonIgnore] internal EntityBatchResult? Result { get; set; } + + internal void ThrowIfFailed() + { + if (this.Result == null) + { + throw new InvalidOperationException("Entity batch request has not been processed yet."); + } + + if (this.Result.FailureDetails is { } f) + { + throw new EntityFailureException(f.ErrorMessage); + } + + List? errors = null; + if (this.Result.Results is not null) + { + foreach (OperationResult result in this.Result.Results) + { + if (result.FailureDetails is { } failure) + { + errors ??= new List(); + errors.Add(new EntityFailureException(failure.ErrorMessage)); + } + } + } + + if (errors is not null) + { + throw errors.Count == 1 ? errors[0] : new AggregateException(errors); + } + } } } diff --git a/src/WebJobs.Extensions.DurableTask/ContextImplementations/RemoteOrchestratorContext.cs b/src/WebJobs.Extensions.DurableTask/ContextImplementations/RemoteOrchestratorContext.cs index a1d9319e3..8cc630bf8 100644 --- a/src/WebJobs.Extensions.DurableTask/ContextImplementations/RemoteOrchestratorContext.cs +++ b/src/WebJobs.Extensions.DurableTask/ContextImplementations/RemoteOrchestratorContext.cs @@ -6,6 +6,7 @@ using DurableTask.Core; using DurableTask.Core.Command; using DurableTask.Core.Entities; +using DurableTask.Core.Exceptions; using DurableTask.Core.History; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -18,6 +19,8 @@ internal class RemoteOrchestratorContext private OrchestratorExecutionResult? executionResult; + private Exception? failure; + public RemoteOrchestratorContext(OrchestrationRuntimeState runtimeState, TaskOrchestrationEntityParameters? entityParameters) { this.runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState)); @@ -48,6 +51,19 @@ public RemoteOrchestratorContext(OrchestrationRuntimeState runtimeState, TaskOrc [JsonIgnore] internal TaskOrchestrationEntityParameters? EntityParameters { get; private set; } + internal void ThrowIfFailed() + { + if (this.failure != null) + { + throw this.failure; + } + } + + internal OrchestratorExecutionResult GetResult() + { + return this.executionResult ?? throw new InvalidOperationException($"The execution result has not yet been set using {nameof(this.SetResult)}."); + } + internal void SetResult(IEnumerable actions, string customStatus) { var result = new OrchestratorExecutionResult @@ -107,16 +123,24 @@ private void SetResultInternal(OrchestratorExecutionResult result) this.OrchestratorCompleted = true; this.SerializedOutput = completeAction.Result; this.ContinuedAsNew = completeAction.OrchestrationStatus == OrchestrationStatus.ContinuedAsNew; + + if (completeAction.OrchestrationStatus == OrchestrationStatus.Failed) + { + string message = completeAction switch + { + { FailureDetails: { } f } => f.ErrorMessage, + { Result: { } r } => r, + _ => "Exception occurred during orchestration execution.", + }; + + this.failure = new OrchestrationFailureException(message); + } + break; } } this.executionResult = result; } - - internal OrchestratorExecutionResult GetResult() - { - return this.executionResult ?? throw new InvalidOperationException($"The execution result has not yet been set using {nameof(this.SetResult)}."); - } } } diff --git a/src/WebJobs.Extensions.DurableTask/Exceptions/EntityFailureException.cs b/src/WebJobs.Extensions.DurableTask/Exceptions/EntityFailureException.cs new file mode 100644 index 000000000..8c455f85a --- /dev/null +++ b/src/WebJobs.Extensions.DurableTask/Exceptions/EntityFailureException.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; + +// TODO: move to DurableTask.Core if needed. +namespace DurableTask.Core.Entities +{ + internal class EntityFailureException : Exception + { + public EntityFailureException(string message) + : base(message) + { + } + } +} diff --git a/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs b/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs index 65751729b..ac7040b2f 100644 --- a/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs +++ b/src/WebJobs.Extensions.DurableTask/OutOfProcMiddleware.cs @@ -14,7 +14,7 @@ using DurableTask.Core.History; using DurableTask.Core.Middleware; using Microsoft.Azure.WebJobs.Host.Executors; -using Newtonsoft.Json; +using P = Microsoft.DurableTask.Protobuf; namespace Microsoft.Azure.WebJobs.Extensions.DurableTask { @@ -137,10 +137,12 @@ await this.LifeCycleNotificationHelper.OrchestratorStartingAsync( } byte[] triggerReturnValueBytes = Convert.FromBase64String(triggerReturnValue); - var response = Microsoft.DurableTask.Protobuf.OrchestratorResponse.Parser.ParseFrom(triggerReturnValueBytes); + P.OrchestratorResponse response = P.OrchestratorResponse.Parser.ParseFrom(triggerReturnValueBytes); context.SetResult( response.Actions.Select(ProtobufUtils.ToOrchestratorAction), response.CustomStatus); + + context.ThrowIfFailed(); }, #pragma warning restore CS0618 // Type or member is obsolete (not intended for general public use) }; @@ -326,9 +328,10 @@ void SetErrorResult(FailureDetails failureDetails) } byte[] triggerReturnValueBytes = Convert.FromBase64String(triggerReturnValue); - var response = Microsoft.DurableTask.Protobuf.EntityBatchResult.Parser.ParseFrom(triggerReturnValueBytes); + P.EntityBatchResult response = P.EntityBatchResult.Parser.ParseFrom(triggerReturnValueBytes); context.Result = response.ToEntityBatchResult(); + context.ThrowIfFailed(); #pragma warning restore CS0618 // Type or member is obsolete (not intended for general public use) }, };