From d5a80b0bbb284d98e7dad45454bc2b8af5fa02be Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Thu, 13 Nov 2025 12:36:47 -0800 Subject: [PATCH 1/4] first commit --- src/Client/Core/DurableTaskClient.cs | 15 ++++++++- src/Client/Grpc/GrpcDurableTaskClient.cs | 33 +++++++++++++++++++ .../ShimDurableTaskClient.cs | 25 ++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/Client/Core/DurableTaskClient.cs b/src/Client/Core/DurableTaskClient.cs index f2d658d8f..c0463bf92 100644 --- a/src/Client/Core/DurableTaskClient.cs +++ b/src/Client/Core/DurableTaskClient.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.ComponentModel; +using DurableTask.Core.History; using Microsoft.DurableTask.Client.Entities; using Microsoft.DurableTask.Internal; @@ -464,7 +465,19 @@ public virtual Task RewindInstanceAsync( string instanceId, string reason, CancellationToken cancellation = default) - => throw new NotSupportedException($"{this.GetType()} does not support orchestration rewind."); + => throw new NotSupportedException($"{this.GetType()} does not support orchestration rewind."); + + /// + /// Retrieves the history of the specified orchestration instance as a list of objects. + /// + /// The instance ID of the orchestration. + /// The execution ID of the orchestration. If null, the history for the most recent execution is retrieved. + /// The cancellation token. + /// The list of objects representing the orchestration's history. + public abstract Task> GetOrchestrationHistoryAsync( + string instanceId, + string? executionId = null, + CancellationToken cancellation = default); // TODO: Create task hub diff --git a/src/Client/Grpc/GrpcDurableTaskClient.cs b/src/Client/Grpc/GrpcDurableTaskClient.cs index efc6d765c..a9f5589f4 100644 --- a/src/Client/Grpc/GrpcDurableTaskClient.cs +++ b/src/Client/Grpc/GrpcDurableTaskClient.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Text; +using DurableTask.Core.History; using Google.Protobuf.WellKnownTypes; using Microsoft.DurableTask.Client.Entities; using Microsoft.DurableTask.Tracing; @@ -468,6 +469,38 @@ public override async Task RewindInstanceAsync( } } + /// + public override async Task> GetOrchestrationHistoryAsync( + string instanceId, + string? executionId = null, + CancellationToken cancellation = default) + { + Check.NotEntity(this.options.EnableEntitySupport, instanceId); + + if (string.IsNullOrEmpty(instanceId)) + { + throw new ArgumentNullException(nameof(instanceId)); + } + + P.StreamInstanceHistoryRequest streamRequest = new() + { + InstanceId = instanceId, + ExecutionId = executionId, + ForWorkItemProcessing = false, + }; + + using AsyncServerStreamingCall streamResponse = + this.sidecarClient.StreamInstanceHistory(streamRequest, cancellationToken: cancellation); + + List pastEvents = []; + while (await streamResponse.ResponseStream.MoveNext(cancellation)) + { + pastEvents.AddRange(streamResponse.ResponseStream.Current.Events.Select(DurableTask.ProtoUtils.ConvertHistoryEvent)); + } + + return pastEvents; + } + static AsyncDisposable GetCallInvoker(GrpcDurableTaskClientOptions options, out CallInvoker callInvoker) { if (options.Channel is GrpcChannel c) diff --git a/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs b/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs index 4fbf828d0..c048b5db0 100644 --- a/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs +++ b/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs @@ -10,6 +10,7 @@ using Microsoft.DurableTask.Client.Entities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +using Newtonsoft.Json; using Core = DurableTask.Core; using CoreOrchestrationQuery = DurableTask.Core.Query.OrchestrationQuery; @@ -307,6 +308,30 @@ public override async Task RestartAsync( return newInstanceId; } + /// + public override async Task> GetOrchestrationHistoryAsync( + string instanceId, + string? executionId = null, + CancellationToken cancellation = default) + { + Check.NotEntity(this.options.EnableEntitySupport, instanceId); + + if (string.IsNullOrEmpty(instanceId)) + { + throw new ArgumentNullException(nameof(instanceId)); + } + + string jsonHistory = await this.Client.GetOrchestrationHistoryAsync(instanceId, executionId); + List? historyEvents = JsonConvert.DeserializeObject>(jsonHistory); + if (historyEvents == null) + { + // We don't send a history in this case. Should we throw an exception instead? + return []; + } + + return historyEvents; + } + [return: NotNullIfNotNull("state")] OrchestrationMetadata? ToMetadata(Core.OrchestrationState? state, bool getInputsAndOutputs) { From 7278d248066e12e25476dd9543d45a0e22e64736 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Mon, 1 Dec 2025 14:11:13 -0800 Subject: [PATCH 2/4] more updated --- src/Client/Core/DurableTaskClient.cs | 9 +++-- src/Client/Grpc/GrpcDurableTaskClient.cs | 35 +++++++++++-------- .../ShimDurableTaskClient.cs | 3 +- src/Shared/Grpc/ProtoUtils.cs | 3 +- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/Client/Core/DurableTaskClient.cs b/src/Client/Core/DurableTaskClient.cs index c0463bf92..bc63fade9 100644 --- a/src/Client/Core/DurableTaskClient.cs +++ b/src/Client/Core/DurableTaskClient.cs @@ -457,7 +457,8 @@ public virtual Task RestartAsync( /// Thrown if this implementation of does not /// support rewinding orchestrations. /// Thrown if the backend storage provider does not support rewinding orchestrations. - /// Thrown if an orchestration with the specified does not exist. + /// Thrown if an orchestration with the specified does not exist, + /// or if is the instance ID of an entity. /// Thrown if a precondition of the operation fails, for example if the specified /// orchestration is not in a "Failed" state. /// Thrown if the operation is canceled via the token. @@ -471,12 +472,14 @@ public virtual Task RewindInstanceAsync( /// Retrieves the history of the specified orchestration instance as a list of objects. /// /// The instance ID of the orchestration. - /// The execution ID of the orchestration. If null, the history for the most recent execution is retrieved. /// The cancellation token. /// The list of objects representing the orchestration's history. + /// Thrown if is null. + /// Thrown if an orchestration with the specified does not exist, + /// or if is the instance ID of an entity. + /// Thrown if the operation is canceled via the token. public abstract Task> GetOrchestrationHistoryAsync( string instanceId, - string? executionId = null, CancellationToken cancellation = default); // TODO: Create task hub diff --git a/src/Client/Grpc/GrpcDurableTaskClient.cs b/src/Client/Grpc/GrpcDurableTaskClient.cs index a9f5589f4..ff44496d6 100644 --- a/src/Client/Grpc/GrpcDurableTaskClient.cs +++ b/src/Client/Grpc/GrpcDurableTaskClient.cs @@ -472,33 +472,40 @@ public override async Task RewindInstanceAsync( /// public override async Task> GetOrchestrationHistoryAsync( string instanceId, - string? executionId = null, CancellationToken cancellation = default) { + Check.NotNullOrEmpty(instanceId); Check.NotEntity(this.options.EnableEntitySupport, instanceId); - if (string.IsNullOrEmpty(instanceId)) - { - throw new ArgumentNullException(nameof(instanceId)); - } - P.StreamInstanceHistoryRequest streamRequest = new() { InstanceId = instanceId, - ExecutionId = executionId, ForWorkItemProcessing = false, }; - using AsyncServerStreamingCall streamResponse = - this.sidecarClient.StreamInstanceHistory(streamRequest, cancellationToken: cancellation); + try + { + using AsyncServerStreamingCall streamResponse = + this.sidecarClient.StreamInstanceHistory(streamRequest, cancellationToken: cancellation); - List pastEvents = []; - while (await streamResponse.ResponseStream.MoveNext(cancellation)) + List pastEvents = []; + while (await streamResponse.ResponseStream.MoveNext(cancellation)) + { + pastEvents.AddRange(streamResponse.ResponseStream.Current.Events.Select(DurableTask.ProtoUtils.ConvertHistoryEvent)); + } + + return pastEvents; + } + catch (RpcException e) when (e.StatusCode == StatusCode.NotFound) { - pastEvents.AddRange(streamResponse.ResponseStream.Current.Events.Select(DurableTask.ProtoUtils.ConvertHistoryEvent)); + throw new ArgumentException($"An orchestration with the instanceId {instanceId} was not found.", e); } - - return pastEvents; + catch (RpcException e) when (e.StatusCode == StatusCode.Cancelled) + { + throw new OperationCanceledException( + $"The {nameof(this.GetOrchestrationHistoryAsync)} operation was canceled.", e, cancellation); + } + // should we add a case for an internal error? I can't just throw an "Exception" in that case, it's too generic apparently } static AsyncDisposable GetCallInvoker(GrpcDurableTaskClientOptions options, out CallInvoker callInvoker) diff --git a/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs b/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs index c048b5db0..c7698e38b 100644 --- a/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs +++ b/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs @@ -311,7 +311,6 @@ public override async Task RestartAsync( /// public override async Task> GetOrchestrationHistoryAsync( string instanceId, - string? executionId = null, CancellationToken cancellation = default) { Check.NotEntity(this.options.EnableEntitySupport, instanceId); @@ -321,7 +320,7 @@ public override async Task> GetOrchestrationHistoryAsync( throw new ArgumentNullException(nameof(instanceId)); } - string jsonHistory = await this.Client.GetOrchestrationHistoryAsync(instanceId, executionId); + string jsonHistory = await this.Client.GetOrchestrationHistoryAsync(instanceId, executionId: null); List? historyEvents = JsonConvert.DeserializeObject>(jsonHistory); if (historyEvents == null) { diff --git a/src/Shared/Grpc/ProtoUtils.cs b/src/Shared/Grpc/ProtoUtils.cs index f7036d28e..ab5551d45 100644 --- a/src/Shared/Grpc/ProtoUtils.cs +++ b/src/Shared/Grpc/ProtoUtils.cs @@ -80,7 +80,8 @@ internal static HistoryEvent ConvertHistoryEvent(P.HistoryEvent proto, EntityCon historyEvent = new ExecutionCompletedEvent( proto.EventId, proto.ExecutionCompleted.Result, - proto.ExecutionCompleted.OrchestrationStatus.ToCore()); + proto.ExecutionCompleted.OrchestrationStatus.ToCore(), + proto.ExecutionCompleted.FailureDetails.ToCore()); break; case P.HistoryEvent.EventTypeOneofCase.ExecutionTerminated: historyEvent = new ExecutionTerminatedEvent(proto.EventId, proto.ExecutionTerminated.Input); From c5b87ef4973c634c1ce85ebf403d434eaca0bf54 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Tue, 2 Dec 2025 12:28:41 -0800 Subject: [PATCH 3/4] removed the API from the shim client --- src/Client/Core/DurableTaskClient.cs | 8 +++++-- src/Client/Grpc/GrpcDurableTaskClient.cs | 6 ++++- .../ShimDurableTaskClient.cs | 24 ------------------- 3 files changed, 11 insertions(+), 27 deletions(-) diff --git a/src/Client/Core/DurableTaskClient.cs b/src/Client/Core/DurableTaskClient.cs index bc63fade9..5279e725b 100644 --- a/src/Client/Core/DurableTaskClient.cs +++ b/src/Client/Core/DurableTaskClient.cs @@ -474,13 +474,17 @@ public virtual Task RewindInstanceAsync( /// The instance ID of the orchestration. /// The cancellation token. /// The list of objects representing the orchestration's history. + /// Thrown if this implementation of does not + /// support retrieving orchestration history. /// Thrown if is null. /// Thrown if an orchestration with the specified does not exist, /// or if is the instance ID of an entity. /// Thrown if the operation is canceled via the token. - public abstract Task> GetOrchestrationHistoryAsync( + /// Thrown if an internal error occurs when attempting to retrieve the orchestration history. + public virtual Task> GetOrchestrationHistoryAsync( string instanceId, - CancellationToken cancellation = default); + CancellationToken cancellation = default) + => throw new NotSupportedException($"{this.GetType()} does not support retrieving orchestration history."); // TODO: Create task hub diff --git a/src/Client/Grpc/GrpcDurableTaskClient.cs b/src/Client/Grpc/GrpcDurableTaskClient.cs index ff44496d6..ab24f2e0f 100644 --- a/src/Client/Grpc/GrpcDurableTaskClient.cs +++ b/src/Client/Grpc/GrpcDurableTaskClient.cs @@ -505,7 +505,11 @@ public override async Task> GetOrchestrationHistoryAsync( throw new OperationCanceledException( $"The {nameof(this.GetOrchestrationHistoryAsync)} operation was canceled.", e, cancellation); } - // should we add a case for an internal error? I can't just throw an "Exception" in that case, it's too generic apparently + catch (RpcException e) when (e.StatusCode == StatusCode.Internal) + { + throw new InvalidOperationException( + $"An error occurred while retrieving the history for orchestration with instanceId {instanceId}.", e); + } } static AsyncDisposable GetCallInvoker(GrpcDurableTaskClientOptions options, out CallInvoker callInvoker) diff --git a/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs b/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs index c7698e38b..4fbf828d0 100644 --- a/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs +++ b/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs @@ -10,7 +10,6 @@ using Microsoft.DurableTask.Client.Entities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Newtonsoft.Json; using Core = DurableTask.Core; using CoreOrchestrationQuery = DurableTask.Core.Query.OrchestrationQuery; @@ -308,29 +307,6 @@ public override async Task RestartAsync( return newInstanceId; } - /// - public override async Task> GetOrchestrationHistoryAsync( - string instanceId, - CancellationToken cancellation = default) - { - Check.NotEntity(this.options.EnableEntitySupport, instanceId); - - if (string.IsNullOrEmpty(instanceId)) - { - throw new ArgumentNullException(nameof(instanceId)); - } - - string jsonHistory = await this.Client.GetOrchestrationHistoryAsync(instanceId, executionId: null); - List? historyEvents = JsonConvert.DeserializeObject>(jsonHistory); - if (historyEvents == null) - { - // We don't send a history in this case. Should we throw an exception instead? - return []; - } - - return historyEvents; - } - [return: NotNullIfNotNull("state")] OrchestrationMetadata? ToMetadata(Core.OrchestrationState? state, bool getInputsAndOutputs) { From 9722042777033851fd66065b62ff9c3129ab5585 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Fri, 5 Dec 2025 11:15:01 -0800 Subject: [PATCH 4/4] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c426e8f2c..5366099d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Update project to target .net 8.0 and .net 10 and upgrade dependencies by Tomer Rosenthal ([#510](https://github.com/microsoft/durabletask-dotnet/pull/510)) - Support worker features announcement by wangbill ([#502](https://github.com/microsoft/durabletask-dotnet/pull/502)) - Introduce custom copilot review instructions by halspang ([#503](https://github.com/microsoft/durabletask-dotnet/pull/503)) +- Add API to get orchestration history ([#516](https://github.com/microsoft/durabletask-dotnet/pull/516)) ## v1.17.1 - Fix Worker Registry and Add Documentation Notes by nytian in [#462](https://github.com/microsoft/durabletask-dotnet/pull/495)