From 8e0a13ab3d149d6cfbea9e4b7cc3bc55b621843e Mon Sep 17 00:00:00 2001 From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:21:59 -0700 Subject: [PATCH 1/5] Add opt-in Timeout to PurgeInstancesFilter for partial purge - Add TimeSpan? Timeout property to PurgeInstancesFilter (opt-in, default null) - Send timeout as google.protobuf.Duration in gRPC request when set - Add timeout field (4) to PurgeInstanceFilter proto message - Zero breaking changes: existing callers unaffected --- src/Client/Core/PurgeInstancesFilter.cs | 8 ++++++++ src/Client/Grpc/GrpcDurableTaskClient.cs | 5 +++++ src/Grpc/orchestrator_service.proto | 1 + 3 files changed, 14 insertions(+) diff --git a/src/Client/Core/PurgeInstancesFilter.cs b/src/Client/Core/PurgeInstancesFilter.cs index 04c7f9c3f..283c56cf2 100644 --- a/src/Client/Core/PurgeInstancesFilter.cs +++ b/src/Client/Core/PurgeInstancesFilter.cs @@ -14,4 +14,12 @@ public record PurgeInstancesFilter( DateTimeOffset? CreatedTo = null, IEnumerable? Statuses = null) { + /// + /// Gets or sets the maximum amount of time to spend purging instances in a single call. + /// If null (default), all matching instances are purged with no time limit. + /// When set, the purge stops accepting new instances after this duration elapses + /// and returns with set to false. + /// Already-started instance deletions will complete before the method returns. + /// + public TimeSpan? Timeout { get; init; } } diff --git a/src/Client/Grpc/GrpcDurableTaskClient.cs b/src/Client/Grpc/GrpcDurableTaskClient.cs index eba7b9bc0..cd445ad0c 100644 --- a/src/Client/Grpc/GrpcDurableTaskClient.cs +++ b/src/Client/Grpc/GrpcDurableTaskClient.cs @@ -488,6 +488,11 @@ public override Task PurgeAllInstancesAsync( request.PurgeInstanceFilter.RuntimeStatus.AddRange(filter.Statuses.Select(x => x.ToGrpcStatus())); } + if (filter?.Timeout is not null) + { + request.PurgeInstanceFilter.Timeout = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(filter.Timeout.Value); + } + return this.PurgeInstancesCoreAsync(request, cancellation); } diff --git a/src/Grpc/orchestrator_service.proto b/src/Grpc/orchestrator_service.proto index 0c34d986d..70ab6de24 100644 --- a/src/Grpc/orchestrator_service.proto +++ b/src/Grpc/orchestrator_service.proto @@ -517,6 +517,7 @@ message PurgeInstanceFilter { google.protobuf.Timestamp createdTimeFrom = 1; google.protobuf.Timestamp createdTimeTo = 2; repeated OrchestrationStatus runtimeStatus = 3; + google.protobuf.Duration timeout = 4; } message PurgeInstancesResponse { From 8cb17f2c9f645712fdef3a2111d3caee00508510 Mon Sep 17 00:00:00 2001 From: wangbill Date: Thu, 19 Mar 2026 14:12:50 -0700 Subject: [PATCH 2/5] Address PR review: add timeout validation, update docs, add unit tests - Add ArgumentOutOfRangeException for zero/negative Timeout in PurgeAllInstancesAsync - Update PurgeInstancesFilter.Timeout XML docs to note backend-dependent semantics - Add 3 unit tests for Timeout validation (negative, zero, positive) --- src/Client/Core/PurgeInstancesFilter.cs | 8 ++-- src/Client/Grpc/GrpcDurableTaskClient.cs | 8 ++++ .../Grpc.Tests/GrpcDurableTaskClientTests.cs | 39 +++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/Client/Core/PurgeInstancesFilter.cs b/src/Client/Core/PurgeInstancesFilter.cs index 283c56cf2..c195025ba 100644 --- a/src/Client/Core/PurgeInstancesFilter.cs +++ b/src/Client/Core/PurgeInstancesFilter.cs @@ -17,9 +17,11 @@ public record PurgeInstancesFilter( /// /// Gets or sets the maximum amount of time to spend purging instances in a single call. /// If null (default), all matching instances are purged with no time limit. - /// When set, the purge stops accepting new instances after this duration elapses - /// and returns with set to false. - /// Already-started instance deletions will complete before the method returns. + /// When set, the purge stops accepting new instances after this duration elapses. + /// The value of depends on the backend implementation: + /// it may be false if the purge timed out, true if all instances were purged, + /// or null if the backend does not support reporting completion status. + /// Not all backends support this property; those that do not will ignore it. /// public TimeSpan? Timeout { get; init; } } diff --git a/src/Client/Grpc/GrpcDurableTaskClient.cs b/src/Client/Grpc/GrpcDurableTaskClient.cs index cd445ad0c..000197eeb 100644 --- a/src/Client/Grpc/GrpcDurableTaskClient.cs +++ b/src/Client/Grpc/GrpcDurableTaskClient.cs @@ -490,6 +490,14 @@ public override Task PurgeAllInstancesAsync( if (filter?.Timeout is not null) { + if (filter.Timeout.Value <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException( + nameof(filter), + filter.Timeout.Value, + "Timeout must be a positive TimeSpan."); + } + request.PurgeInstanceFilter.Timeout = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(filter.Timeout.Value); } diff --git a/test/Client/Grpc.Tests/GrpcDurableTaskClientTests.cs b/test/Client/Grpc.Tests/GrpcDurableTaskClientTests.cs index 8d098106c..f3da85ebf 100644 --- a/test/Client/Grpc.Tests/GrpcDurableTaskClientTests.cs +++ b/test/Client/Grpc.Tests/GrpcDurableTaskClientTests.cs @@ -127,5 +127,44 @@ public async Task ScheduleNewOrchestrationInstanceAsync_ValidDedupeStatus_DoesNo var exception = await act.Should().ThrowAsync(); exception.Which.Should().NotBeOfType(); } + + [Fact] + public async Task PurgeAllInstancesAsync_NegativeTimeout_ThrowsArgumentOutOfRangeException() + { + // Arrange + var client = this.CreateClient(); + var filter = new PurgeInstancesFilter { Timeout = TimeSpan.FromSeconds(-1) }; + + // Act & Assert + Func act = async () => await client.PurgeAllInstancesAsync(filter); + var exception = await act.Should().ThrowAsync(); + exception.Which.Message.Should().Contain("Timeout must be a positive TimeSpan."); + } + + [Fact] + public async Task PurgeAllInstancesAsync_ZeroTimeout_ThrowsArgumentOutOfRangeException() + { + // Arrange + var client = this.CreateClient(); + var filter = new PurgeInstancesFilter { Timeout = TimeSpan.Zero }; + + // Act & Assert + Func act = async () => await client.PurgeAllInstancesAsync(filter); + var exception = await act.Should().ThrowAsync(); + exception.Which.Message.Should().Contain("Timeout must be a positive TimeSpan."); + } + + [Fact] + public async Task PurgeAllInstancesAsync_PositiveTimeout_DoesNotThrowValidationError() + { + // Arrange + var client = this.CreateClient(); + var filter = new PurgeInstancesFilter { Timeout = TimeSpan.FromSeconds(30) }; + + // Act & Assert - validation should pass; the call will fail at gRPC level, not validation + Func act = async () => await client.PurgeAllInstancesAsync(filter); + var exception = await act.Should().ThrowAsync(); + exception.Which.Should().NotBeOfType(); + } } From bf694080205ebfb73fec6487bb156b9bc3cce1e9 Mon Sep 17 00:00:00 2001 From: wangbill Date: Fri, 20 Mar 2026 15:44:00 -0700 Subject: [PATCH 3/5] Update timeout description in PurgeInstancesFilter for clarity on partial results --- src/Client/Core/PurgeInstancesFilter.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Client/Core/PurgeInstancesFilter.cs b/src/Client/Core/PurgeInstancesFilter.cs index c195025ba..4c20e2cde 100644 --- a/src/Client/Core/PurgeInstancesFilter.cs +++ b/src/Client/Core/PurgeInstancesFilter.cs @@ -17,7 +17,9 @@ public record PurgeInstancesFilter( /// /// Gets or sets the maximum amount of time to spend purging instances in a single call. /// If null (default), all matching instances are purged with no time limit. - /// When set, the purge stops accepting new instances after this duration elapses. + /// When set, the purge operation stops deleting additional instances after this duration elapses + /// and returns a partial result. Callers can check and + /// re-invoke the purge to continue where it left off. /// The value of depends on the backend implementation: /// it may be false if the purge timed out, true if all instances were purged, /// or null if the backend does not support reporting completion status. From 61fe91d8435c1a34a12f7b1750955956733a3905 Mon Sep 17 00:00:00 2001 From: wangbill Date: Thu, 2 Apr 2026 18:34:03 -0700 Subject: [PATCH 4/5] Improve ArgumentOutOfRangeException message to specify Timeout property --- src/Client/Grpc/GrpcDurableTaskClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client/Grpc/GrpcDurableTaskClient.cs b/src/Client/Grpc/GrpcDurableTaskClient.cs index 000197eeb..46e4dd2ed 100644 --- a/src/Client/Grpc/GrpcDurableTaskClient.cs +++ b/src/Client/Grpc/GrpcDurableTaskClient.cs @@ -495,7 +495,7 @@ public override Task PurgeAllInstancesAsync( throw new ArgumentOutOfRangeException( nameof(filter), filter.Timeout.Value, - "Timeout must be a positive TimeSpan."); + "PurgeInstancesFilter.Timeout must be a positive TimeSpan."); } request.PurgeInstanceFilter.Timeout = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan(filter.Timeout.Value); From 5e82dd42aceb2703223709bef003e1a9af190315 Mon Sep 17 00:00:00 2001 From: wangbill Date: Mon, 6 Apr 2026 09:11:17 -0700 Subject: [PATCH 5/5] =?UTF-8?q?Refresh=20proto=20from=20upstream=20(bcf5af?= =?UTF-8?q?6)=20=E2=80=94=20includes=20timeout=20field=20and=20other=20add?= =?UTF-8?q?itions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Grpc/orchestrator_service.proto | 6 ++++++ src/Grpc/versions.txt | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Grpc/orchestrator_service.proto b/src/Grpc/orchestrator_service.proto index 70ab6de24..3d7c8eb49 100644 --- a/src/Grpc/orchestrator_service.proto +++ b/src/Grpc/orchestrator_service.proto @@ -25,6 +25,7 @@ message ActivityRequest { OrchestrationInstance orchestrationInstance = 4; int32 taskId = 5; TraceContext parentTraceContext = 6; + map tags = 7; } message ActivityResponse { @@ -320,6 +321,10 @@ message SendEntityMessageAction { } } +message RewindOrchestrationAction { + repeated HistoryEvent newHistory = 1; +} + message OrchestratorAction { int32 id = 1; oneof orchestratorActionType { @@ -330,6 +335,7 @@ message OrchestratorAction { CompleteOrchestrationAction completeOrchestration = 6; TerminateOrchestrationAction terminateOrchestration = 7; SendEntityMessageAction sendEntityMessage = 8; + RewindOrchestrationAction rewindOrchestration = 9; } } diff --git a/src/Grpc/versions.txt b/src/Grpc/versions.txt index 743f3f8bd..b781a3903 100644 --- a/src/Grpc/versions.txt +++ b/src/Grpc/versions.txt @@ -1,2 +1,2 @@ -# The following files were downloaded from branch main at 2026-02-24 00:01:28 UTC -https://raw.githubusercontent.com/microsoft/durabletask-protobuf/1caadbd7ecfdf5f2309acbeac28a3e36d16aa156/protos/orchestrator_service.proto +# The following files were downloaded from branch main at 2026-04-06 16:10:08 UTC +https://raw.githubusercontent.com/microsoft/durabletask-protobuf/bcf5af6a22caa70601bfc909918ba5937484279f/protos/orchestrator_service.proto