From 8357e15a39719a312c0fa4ce9dd2c309044c1987 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Tue, 20 Jan 2026 17:33:33 -0800 Subject: [PATCH 1/6] first commit? --- src/Client/Grpc/GrpcDurableTaskClient.cs | 24 +- src/Client/Grpc/ProtoUtils.cs | 52 ++-- .../Sidecar/Grpc/TaskHubGrpcServer.cs | 2 +- test/Client/Grpc.Tests/ProtoUtilsTests.cs | 230 ++++++++---------- 4 files changed, 136 insertions(+), 172 deletions(-) diff --git a/src/Client/Grpc/GrpcDurableTaskClient.cs b/src/Client/Grpc/GrpcDurableTaskClient.cs index 20cc348b..669c1045 100644 --- a/src/Client/Grpc/GrpcDurableTaskClient.cs +++ b/src/Client/Grpc/GrpcDurableTaskClient.cs @@ -124,12 +124,9 @@ public override async Task ScheduleNewOrchestrationInstanceAsync( } // Set orchestration ID reuse policy for deduplication support - // Note: This requires the protobuf to support OrchestrationIdReusePolicy field - // If the protobuf doesn't support it yet, this will need to be updated when the protobuf is updated - if (options?.DedupeStatuses != null && options.DedupeStatuses.Count > 0) - { - // Parse and validate all status strings to enum first - ImmutableHashSet dedupeStatuses = options.DedupeStatuses + ImmutableHashSet dedupeStatuses = options?.DedupeStatuses is null + ? [] + : [.. options.DedupeStatuses .Select(s => { if (!System.Enum.TryParse(s, ignoreCase: true, out OrchestrationRuntimeStatus status)) @@ -139,16 +136,15 @@ public override async Task ScheduleNewOrchestrationInstanceAsync( } return status; - }).ToImmutableHashSet(); + })]; - // Convert dedupe statuses to protobuf statuses and create reuse policy - IEnumerable dedupeStatusesProto = dedupeStatuses.Select(s => s.ToGrpcStatus()); - P.OrchestrationIdReusePolicy? policy = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatusesProto); + // Convert dedupe statuses to protobuf statuses and create reuse policy + IEnumerable dedupeStatusesProto = dedupeStatuses.Select(s => s.ToGrpcStatus()); + P.OrchestrationIdReusePolicy? policy = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatusesProto); - if (policy != null) - { - request.OrchestrationIdReusePolicy = policy; - } + if (policy != null) + { + request.OrchestrationIdReusePolicy = policy; } using Activity? newActivity = TraceHelper.StartActivityForNewOrchestration(request); diff --git a/src/Client/Grpc/ProtoUtils.cs b/src/Client/Grpc/ProtoUtils.cs index f307f43f..1a83d1f2 100644 --- a/src/Client/Grpc/ProtoUtils.cs +++ b/src/Client/Grpc/ProtoUtils.cs @@ -13,18 +13,22 @@ namespace Microsoft.DurableTask.Client.Grpc; public static class ProtoUtils { /// - /// Gets the terminal orchestration statuses that are commonly used for deduplication. + /// Gets an array of all orchestration statuses. /// These are the statuses that can be used in OrchestrationIdReusePolicy. /// - /// An immutable array of terminal orchestration statuses. - public static ImmutableArray GetTerminalStatuses() + /// An immutable array of all orchestration statuses. + public static ImmutableArray GetAllStatuses() { -#pragma warning disable CS0618 // Type or member is obsolete - Canceled is intentionally included for compatibility +#pragma warning disable CS0618 // Type or member is obsolete - Canceled is intentionally included for compatibility + // compatibility with what? return ImmutableArray.Create( P.OrchestrationStatus.Completed, P.OrchestrationStatus.Failed, - P.OrchestrationStatus.Terminated, - P.OrchestrationStatus.Canceled); + P.OrchestrationStatus.Terminated, + P.OrchestrationStatus.Canceled, + P.OrchestrationStatus.Pending, + P.OrchestrationStatus.Running, + P.OrchestrationStatus.Suspended); #pragma warning restore CS0618 } @@ -33,28 +37,27 @@ public static class ProtoUtils /// with replaceable statuses (statuses that CAN be replaced). /// /// The orchestration statuses that should NOT be replaced. These are statuses for which an exception should be thrown if an orchestration already exists. - /// An OrchestrationIdReusePolicy with replaceable statuses set, or null if all terminal statuses are dedupe statuses. + /// An OrchestrationIdReusePolicy with replaceable statuses set. /// /// The policy uses "replaceableStatus" - these are statuses that CAN be replaced. /// dedupeStatuses are statuses that should NOT be replaced. - /// So replaceableStatus = all terminal statuses MINUS dedupeStatuses. + /// So replaceableStatus = all statuses MINUS dedupeStatuses. /// - public static P.OrchestrationIdReusePolicy? ConvertDedupeStatusesToReusePolicy( + public static P.OrchestrationIdReusePolicy ConvertDedupeStatusesToReusePolicy( IEnumerable? dedupeStatuses) { - ImmutableArray terminalStatuses = GetTerminalStatuses(); + ImmutableArray statuses = GetAllStatuses(); ImmutableHashSet dedupeStatusSet = dedupeStatuses?.ToImmutableHashSet() ?? ImmutableHashSet.Empty; P.OrchestrationIdReusePolicy policy = new(); - // Add terminal statuses that are NOT in dedupeStatuses as replaceable - foreach (P.OrchestrationStatus terminalStatus in terminalStatuses.Where(status => !dedupeStatusSet.Contains(status))) + // Add statuses that are NOT in dedupeStatuses as replaceable + foreach (P.OrchestrationStatus status in statuses.Where(status => !dedupeStatusSet.Contains(status))) { - policy.ReplaceableStatus.Add(terminalStatus); + policy.ReplaceableStatus.Add(status); } - // Only return policy if we have replaceable statuses - return policy.ReplaceableStatus.Count > 0 ? policy : null; + return policy; } /// @@ -62,26 +65,21 @@ public static class ProtoUtils /// (statuses that should NOT be replaced). /// /// The OrchestrationIdReusePolicy containing replaceable statuses. - /// An array of orchestration statuses that should NOT be replaced, or null if all terminal statuses are replaceable. + /// An array of orchestration statuses that should NOT be replaced, or null if all statuses are replaceable. /// /// The policy uses "replaceableStatus" - these are statuses that CAN be replaced. /// dedupeStatuses are statuses that should NOT be replaced (should throw exception). - /// So dedupeStatuses = all terminal statuses MINUS replaceableStatus. + /// So dedupeStatuses = all statuses MINUS replaceableStatus. /// public static P.OrchestrationStatus[]? ConvertReusePolicyToDedupeStatuses( - P.OrchestrationIdReusePolicy? policy) + P.OrchestrationIdReusePolicy policy) { - if (policy == null || policy.ReplaceableStatus.Count == 0) - { - return null; - } - - ImmutableArray terminalStatuses = GetTerminalStatuses(); + ImmutableArray allStatuses = GetAllStatuses(); ImmutableHashSet replaceableStatusSet = policy.ReplaceableStatus.ToImmutableHashSet(); - // Calculate dedupe statuses = terminal statuses - replaceable statuses - P.OrchestrationStatus[] dedupeStatuses = terminalStatuses - .Where(terminalStatus => !replaceableStatusSet.Contains(terminalStatus)) + // Calculate dedupe statuses = all statuses - replaceable statuses + P.OrchestrationStatus[] dedupeStatuses = allStatuses + .Where(status => !replaceableStatusSet.Contains(status)) .ToArray(); // Only return if there are dedupe statuses diff --git a/src/InProcessTestHost/Sidecar/Grpc/TaskHubGrpcServer.cs b/src/InProcessTestHost/Sidecar/Grpc/TaskHubGrpcServer.cs index bbb162c3..ca2207f4 100644 --- a/src/InProcessTestHost/Sidecar/Grpc/TaskHubGrpcServer.cs +++ b/src/InProcessTestHost/Sidecar/Grpc/TaskHubGrpcServer.cs @@ -208,7 +208,7 @@ async Task WaitForWorkItemClientConnection() // Convert OrchestrationIdReusePolicy to dedupeStatuses // The policy uses "replaceableStatus" - these are statuses that CAN be replaced // dedupeStatuses are statuses that should NOT be replaced (should throw exception) - // So dedupeStatuses = all terminal statuses MINUS replaceableStatus + // So dedupeStatuses = all statuses MINUS replaceableStatus OrchestrationStatus[]? dedupeStatuses = null; P.OrchestrationStatus[]? dedupeStatusesProto = ProtoUtils.ConvertReusePolicyToDedupeStatuses(request.OrchestrationIdReusePolicy); if (dedupeStatusesProto != null) diff --git a/test/Client/Grpc.Tests/ProtoUtilsTests.cs b/test/Client/Grpc.Tests/ProtoUtilsTests.cs index 4db7a884..a8669438 100644 --- a/test/Client/Grpc.Tests/ProtoUtilsTests.cs +++ b/test/Client/Grpc.Tests/ProtoUtilsTests.cs @@ -9,18 +9,22 @@ namespace Microsoft.DurableTask.Client.Grpc.Tests; public class ProtoUtilsTests { [Fact] - public void GetTerminalStatuses_ReturnsExpectedStatuses() + public void GetAllStatuses_ReturnsExpectedStatuses() { // Act - ImmutableArray terminalStatuses = ProtoUtils.GetTerminalStatuses(); + ImmutableArray allStatuses = ProtoUtils.GetAllStatuses(); // Assert - terminalStatuses.Should().HaveCount(4); - terminalStatuses.Should().Contain(P.OrchestrationStatus.Completed); - terminalStatuses.Should().Contain(P.OrchestrationStatus.Failed); - terminalStatuses.Should().Contain(P.OrchestrationStatus.Terminated); + allStatuses.Should().HaveCount(7); + allStatuses.Should().Contain(P.OrchestrationStatus.Completed); + allStatuses.Should().Contain(P.OrchestrationStatus.Failed); + allStatuses.Should().Contain(P.OrchestrationStatus.Terminated); #pragma warning disable CS0618 // Type or member is obsolete - terminalStatuses.Should().Contain(P.OrchestrationStatus.Canceled); + allStatuses.Should().Contain(P.OrchestrationStatus.Canceled); + allStatuses.Should().Contain(P.OrchestrationStatus.Pending); + allStatuses.Should().Contain(P.OrchestrationStatus.Running); + allStatuses.Should().Contain(P.OrchestrationStatus.Suspended); + #pragma warning restore CS0618 } @@ -28,11 +32,11 @@ public void GetTerminalStatuses_ReturnsExpectedStatuses() public void GetTerminalStatuses_ReturnsImmutableArray() { // Act - ImmutableArray terminalStatuses = ProtoUtils.GetTerminalStatuses(); + ImmutableArray allStatuses = ProtoUtils.GetAllStatuses(); // Assert - terminalStatuses.IsDefault.Should().BeFalse(); - terminalStatuses.IsEmpty.Should().BeFalse(); + allStatuses.IsDefault.Should().BeFalse(); + allStatuses.IsEmpty.Should().BeFalse(); } [Fact] @@ -42,67 +46,75 @@ public void ConvertDedupeStatusesToReusePolicy_EmptyArray_ReturnsPolicyWithAllTe var dedupeStatuses = Array.Empty(); // Act - P.OrchestrationIdReusePolicy? result = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); + P.OrchestrationIdReusePolicy result = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); // Assert - // Empty array means no dedupe statuses, so all terminal statuses are replaceable + // Empty array means no dedupe statuses, so all statuses are replaceable result.Should().NotBeNull(); - result!.ReplaceableStatus.Should().HaveCount(4); + result!.ReplaceableStatus.Should().HaveCount(7); } [Fact] - public void ConvertDedupeStatusesToReusePolicy_AllTerminalStatuses_ReturnsNull() + public void ConvertDedupeStatusesToReusePolicy_AllStatuses_ReturnsNull() { // Arrange - ImmutableArray allTerminalStatuses = ProtoUtils.GetTerminalStatuses(); - var dedupeStatuses = allTerminalStatuses.ToArray(); + ImmutableArray allStatuses = ProtoUtils.GetAllStatuses(); + var dedupeStatuses = allStatuses.ToArray(); // Act - P.OrchestrationIdReusePolicy? result = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); + P.OrchestrationIdReusePolicy result = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); // Assert - result.Should().BeNull(); + result.ReplaceableStatus.Should().BeEmpty(); } [Fact] - public void ConvertDedupeStatusesToReusePolicy_NoDedupeStatuses_ReturnsPolicyWithAllTerminalStatuses() + public void ConvertDedupeStatusesToReusePolicy_NoDedupeStatuses_ReturnsPolicyWithAllStatuses() { // Arrange var dedupeStatuses = Array.Empty(); // Act - P.OrchestrationIdReusePolicy? result = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); + P.OrchestrationIdReusePolicy result = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); // Assert // When no dedupe statuses, all terminal statuses should be replaceable result.Should().NotBeNull(); - result!.ReplaceableStatus.Should().HaveCount(4); + result!.ReplaceableStatus.Should().HaveCount(7); result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Completed); result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Failed); result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Terminated); #pragma warning disable CS0618 // Type or member is obsolete - result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Canceled); -#pragma warning restore CS0618 + result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Canceled); +#pragma warning restore CS0618 + result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Pending); + result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Running); + result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Suspended); } [Fact] public void ConvertDedupeStatusesToReusePolicy_SingleDedupeStatus_ReturnsPolicyWithRemainingStatuses() { // Arrange - var dedupeStatuses = new[] { P.OrchestrationStatus.Completed }; + var dedupeStatuses = new[] { P.OrchestrationStatus.Running }; // Act - P.OrchestrationIdReusePolicy? result = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); + P.OrchestrationIdReusePolicy result = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); // Assert result.Should().NotBeNull(); - result!.ReplaceableStatus.Should().HaveCount(3); + result!.ReplaceableStatus.Should().HaveCount(6); result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Failed); result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Terminated); #pragma warning disable CS0618 // Type or member is obsolete result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Canceled); #pragma warning restore CS0618 - result.ReplaceableStatus.Should().NotContain(P.OrchestrationStatus.Completed); + result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Completed); + result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Pending); + result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Suspended); + result.ReplaceableStatus.Should().NotContain(P.OrchestrationStatus.Running); + + } [Fact] @@ -112,21 +124,25 @@ public void ConvertDedupeStatusesToReusePolicy_MultipleDedupeStatuses_ReturnsPol var dedupeStatuses = new[] { P.OrchestrationStatus.Completed, - P.OrchestrationStatus.Failed + P.OrchestrationStatus.Failed, + P.OrchestrationStatus.Pending }; // Act - P.OrchestrationIdReusePolicy? result = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); + P.OrchestrationIdReusePolicy result = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); // Assert result.Should().NotBeNull(); - result!.ReplaceableStatus.Should().HaveCount(2); + result!.ReplaceableStatus.Should().HaveCount(4); result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Terminated); #pragma warning disable CS0618 // Type or member is obsolete result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Canceled); -#pragma warning restore CS0618 +#pragma warning restore CS0618 + result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Running); + result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Suspended); result.ReplaceableStatus.Should().NotContain(P.OrchestrationStatus.Completed); - result.ReplaceableStatus.Should().NotContain(P.OrchestrationStatus.Failed); + result.ReplaceableStatus.Should().NotContain(P.OrchestrationStatus.Failed); + result.ReplaceableStatus.Should().NotContain(P.OrchestrationStatus.Pending); } [Fact] @@ -141,57 +157,22 @@ public void ConvertDedupeStatusesToReusePolicy_DuplicateDedupeStatuses_HandlesDu }; // Act - P.OrchestrationIdReusePolicy? result = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); + P.OrchestrationIdReusePolicy result = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); // Assert result.Should().NotBeNull(); - result!.ReplaceableStatus.Should().HaveCount(2); + result!.ReplaceableStatus.Should().HaveCount(5); result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Terminated); #pragma warning disable CS0618 // Type or member is obsolete result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Canceled); -#pragma warning restore CS0618 - } - - [Fact] - public void ConvertDedupeStatusesToReusePolicy_NonTerminalStatus_IgnoresNonTerminalStatus() - { - // Arrange - var dedupeStatuses = new[] - { - P.OrchestrationStatus.Completed, - P.OrchestrationStatus.Running, // Non-terminal status - P.OrchestrationStatus.Pending // Non-terminal status - }; - - // Act - P.OrchestrationIdReusePolicy? result = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); - - // Assert - result.Should().NotBeNull(); - result!.ReplaceableStatus.Should().HaveCount(3); - result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Failed); - result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Terminated); -#pragma warning disable CS0618 // Type or member is obsolete - result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Canceled); -#pragma warning restore CS0618 - result.ReplaceableStatus.Should().NotContain(P.OrchestrationStatus.Completed); - } - - [Fact] - public void ConvertReusePolicyToDedupeStatuses_NullPolicy_ReturnsNull() - { - // Arrange - P.OrchestrationIdReusePolicy? policy = null; - - // Act - P.OrchestrationStatus[]? result = ProtoUtils.ConvertReusePolicyToDedupeStatuses(policy); - - // Assert - result.Should().BeNull(); +#pragma warning restore CS0618 + result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Pending); + result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Running); + result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Suspended); } [Fact] - public void ConvertReusePolicyToDedupeStatuses_EmptyPolicy_ReturnsNull() + public void ConvertReusePolicyToDedupeStatuses_EmptyPolicy_ReturnsAllStatuses() { // Arrange var policy = new P.OrchestrationIdReusePolicy(); @@ -200,16 +181,16 @@ public void ConvertReusePolicyToDedupeStatuses_EmptyPolicy_ReturnsNull() P.OrchestrationStatus[]? result = ProtoUtils.ConvertReusePolicyToDedupeStatuses(policy); // Assert - result.Should().BeNull(); + result.Should().Equal(ProtoUtils.GetAllStatuses()); } [Fact] - public void ConvertReusePolicyToDedupeStatuses_AllTerminalStatusesReplaceable_ReturnsNull() + public void ConvertReusePolicyToDedupeStatuses_AllStatusesReplaceable_ReturnsNull() { // Arrange var policy = new P.OrchestrationIdReusePolicy(); - ImmutableArray terminalStatuses = ProtoUtils.GetTerminalStatuses(); - foreach (var status in terminalStatuses) + ImmutableArray allStatuses = ProtoUtils.GetAllStatuses(); + foreach (var status in allStatuses) { policy.ReplaceableStatus.Add(status); } @@ -233,12 +214,15 @@ public void ConvertReusePolicyToDedupeStatuses_SingleReplaceableStatus_ReturnsRe // Assert result.Should().NotBeNull(); - result!.Should().HaveCount(3); + result!.Should().HaveCount(6); result.Should().Contain(P.OrchestrationStatus.Failed); result.Should().Contain(P.OrchestrationStatus.Terminated); #pragma warning disable CS0618 // Type or member is obsolete result.Should().Contain(P.OrchestrationStatus.Canceled); -#pragma warning restore CS0618 +#pragma warning restore CS0618 + result.Should().Contain(P.OrchestrationStatus.Running); + result.Should().Contain(P.OrchestrationStatus.Pending); + result.Should().Contain(P.OrchestrationStatus.Suspended); result.Should().NotContain(P.OrchestrationStatus.Completed); } @@ -255,38 +239,18 @@ public void ConvertReusePolicyToDedupeStatuses_MultipleReplaceableStatuses_Retur // Assert result.Should().NotBeNull(); - result!.Should().HaveCount(2); + result!.Should().HaveCount(5); result.Should().Contain(P.OrchestrationStatus.Terminated); #pragma warning disable CS0618 // Type or member is obsolete - result.Should().Contain(P.OrchestrationStatus.Canceled); -#pragma warning restore CS0618 + result.Should().Contain(P.OrchestrationStatus.Canceled); +#pragma warning restore CS0618 + result.Should().Contain(P.OrchestrationStatus.Running); + result.Should().Contain(P.OrchestrationStatus.Pending); + result.Should().Contain(P.OrchestrationStatus.Suspended); result.Should().NotContain(P.OrchestrationStatus.Completed); result.Should().NotContain(P.OrchestrationStatus.Failed); } - [Fact] - public void ConvertReusePolicyToDedupeStatuses_NonTerminalStatusInPolicy_IgnoresNonTerminalStatus() - { - // Arrange - var policy = new P.OrchestrationIdReusePolicy(); - policy.ReplaceableStatus.Add(P.OrchestrationStatus.Completed); - policy.ReplaceableStatus.Add(P.OrchestrationStatus.Running); // Non-terminal status - policy.ReplaceableStatus.Add(P.OrchestrationStatus.Pending); // Non-terminal status - - // Act - P.OrchestrationStatus[]? result = ProtoUtils.ConvertReusePolicyToDedupeStatuses(policy); - - // Assert - result.Should().NotBeNull(); - result!.Should().HaveCount(3); - result.Should().Contain(P.OrchestrationStatus.Failed); - result.Should().Contain(P.OrchestrationStatus.Terminated); -#pragma warning disable CS0618 // Type or member is obsolete - result.Should().Contain(P.OrchestrationStatus.Canceled); -#pragma warning restore CS0618 - result.Should().NotContain(P.OrchestrationStatus.Completed); - } - [Fact] public void ConvertReusePolicyToDedupeStatuses_DuplicateReplaceableStatuses_HandlesDuplicates() { @@ -301,11 +265,14 @@ public void ConvertReusePolicyToDedupeStatuses_DuplicateReplaceableStatuses_Hand // Assert result.Should().NotBeNull(); - result!.Should().HaveCount(2); + result!.Should().HaveCount(5); result.Should().Contain(P.OrchestrationStatus.Terminated); #pragma warning disable CS0618 // Type or member is obsolete - result.Should().Contain(P.OrchestrationStatus.Canceled); -#pragma warning restore CS0618 + result.Should().Contain(P.OrchestrationStatus.Canceled); +#pragma warning restore CS0618 + result.Should().Contain(P.OrchestrationStatus.Running); + result.Should().Contain(P.OrchestrationStatus.Pending); + result.Should().Contain(P.OrchestrationStatus.Suspended); } [Fact] @@ -319,7 +286,7 @@ public void ConvertDedupeStatusesToReusePolicy_ThenConvertBack_ReturnsOriginalDe }; // Act - P.OrchestrationIdReusePolicy? policy = ProtoUtils.ConvertDedupeStatusesToReusePolicy(originalDedupeStatuses); + P.OrchestrationIdReusePolicy policy = ProtoUtils.ConvertDedupeStatusesToReusePolicy(originalDedupeStatuses); P.OrchestrationStatus[]? convertedBack = ProtoUtils.ConvertReusePolicyToDedupeStatuses(policy); // Assert @@ -345,19 +312,20 @@ public void ConvertReusePolicyToDedupeStatuses_ThenConvertBack_ReturnsOriginalPo } [Fact] - public void ConvertDedupeStatusesToReusePolicy_AllStatuses_ThenConvertBack_ReturnsNull() + public void ConvertDedupeStatusesToReusePolicy_AllStatuses_ThenConvertBack_IsOriginal() { // Arrange - ImmutableArray allTerminalStatuses = ProtoUtils.GetTerminalStatuses(); - var dedupeStatuses = allTerminalStatuses.ToArray(); + ImmutableArray allStatuses = ProtoUtils.GetAllStatuses(); + var dedupeStatuses = allStatuses.ToArray(); // Act - P.OrchestrationIdReusePolicy? policy = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); + P.OrchestrationIdReusePolicy policy = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); P.OrchestrationStatus[]? convertedBack = ProtoUtils.ConvertReusePolicyToDedupeStatuses(policy); // Assert - policy.Should().BeNull(); - convertedBack.Should().BeNull(); + policy.ReplaceableStatus.Should().BeEmpty(); + convertedBack.Should().NotBeNull(); + convertedBack.Should().Equal(dedupeStatuses); } [Fact] @@ -365,22 +333,22 @@ public void ConvertReusePolicyToDedupeStatuses_AllStatuses_ThenConvertBack_Retur { // Arrange var policy = new P.OrchestrationIdReusePolicy(); - ImmutableArray terminalStatuses = ProtoUtils.GetTerminalStatuses(); - foreach (var status in terminalStatuses) + ImmutableArray allStatuses = ProtoUtils.GetAllStatuses(); + foreach (var status in allStatuses) { policy.ReplaceableStatus.Add(status); } // Act P.OrchestrationStatus[]? dedupeStatuses = ProtoUtils.ConvertReusePolicyToDedupeStatuses(policy); - P.OrchestrationIdReusePolicy? convertedBack = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); + P.OrchestrationIdReusePolicy convertedBack = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); // Assert // Policy with all statuses -> no dedupe statuses -> null // null dedupe statuses -> all are replaceable -> policy with all statuses dedupeStatuses.Should().BeNull(); convertedBack.Should().NotBeNull(); - convertedBack!.ReplaceableStatus.Should().HaveCount(4); + convertedBack!.ReplaceableStatus.Should().HaveCount(7); convertedBack.ReplaceableStatus.Should().BeEquivalentTo(policy.ReplaceableStatus); } @@ -391,7 +359,7 @@ public void ConvertDedupeStatusesToReusePolicy_EmptyArray_ThenConvertBack_Return var dedupeStatuses = Array.Empty(); // Act - P.OrchestrationIdReusePolicy? policy = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); + P.OrchestrationIdReusePolicy policy = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); P.OrchestrationStatus[]? convertedBack = ProtoUtils.ConvertReusePolicyToDedupeStatuses(policy); // Assert @@ -409,20 +377,22 @@ public void ConvertReusePolicyToDedupeStatuses_EmptyPolicy_ThenConvertBack_Retur // Act P.OrchestrationStatus[]? dedupeStatuses = ProtoUtils.ConvertReusePolicyToDedupeStatuses(policy); - P.OrchestrationIdReusePolicy? convertedBack = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); + P.OrchestrationIdReusePolicy convertedBack = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); // Assert - // Empty policy (no replaceable statuses) -> ConvertReusePolicyToDedupeStatuses returns null - // null dedupe statuses -> all terminal statuses are replaceable -> policy with all statuses - dedupeStatuses.Should().BeNull(); - convertedBack.Should().NotBeNull(); - convertedBack!.ReplaceableStatus.Should().HaveCount(4); + // Empty policy (no replaceable statuses) -> ConvertReusePolicyToDedupeStatuses returns all statuses + // all statuses deduped -> no statuses are replaceable -> policy with no statuses + dedupeStatuses.Should().Equal(ProtoUtils.GetAllStatuses()); + convertedBack.ReplaceableStatus.Should().BeEmpty(); } [Theory] [InlineData(P.OrchestrationStatus.Completed)] [InlineData(P.OrchestrationStatus.Failed)] - [InlineData(P.OrchestrationStatus.Terminated)] + [InlineData(P.OrchestrationStatus.Terminated)] + [InlineData(P.OrchestrationStatus.Pending)] + [InlineData(P.OrchestrationStatus.Running)] + [InlineData(P.OrchestrationStatus.Suspended)] public void ConvertDedupeStatusesToReusePolicy_SingleStatus_ThenConvertBack_ReturnsOriginal( P.OrchestrationStatus dedupeStatus) { @@ -430,7 +400,7 @@ public void ConvertDedupeStatusesToReusePolicy_SingleStatus_ThenConvertBack_Retu var dedupeStatuses = new[] { dedupeStatus }; // Act - P.OrchestrationIdReusePolicy? policy = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); + P.OrchestrationIdReusePolicy policy = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); P.OrchestrationStatus[]? convertedBack = ProtoUtils.ConvertReusePolicyToDedupeStatuses(policy); // Assert @@ -451,7 +421,7 @@ public void ConvertDedupeStatusesToReusePolicy_ThreeOutOfFourStatuses_ThenConver }; // Act - P.OrchestrationIdReusePolicy? policy = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); + P.OrchestrationIdReusePolicy policy = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); P.OrchestrationStatus[]? convertedBack = ProtoUtils.ConvertReusePolicyToDedupeStatuses(policy); // Assert From b434062747d8d66d728f092b5e2dfd5a2eed75bb Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Tue, 20 Jan 2026 18:10:14 -0800 Subject: [PATCH 2/6] PR comments --- src/Client/Grpc/GrpcDurableTaskClient.cs | 7 +------ test/Client/Grpc.Tests/ProtoUtilsTests.cs | 11 +++++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/Client/Grpc/GrpcDurableTaskClient.cs b/src/Client/Grpc/GrpcDurableTaskClient.cs index 669c1045..817dbedb 100644 --- a/src/Client/Grpc/GrpcDurableTaskClient.cs +++ b/src/Client/Grpc/GrpcDurableTaskClient.cs @@ -140,12 +140,7 @@ public override async Task ScheduleNewOrchestrationInstanceAsync( // Convert dedupe statuses to protobuf statuses and create reuse policy IEnumerable dedupeStatusesProto = dedupeStatuses.Select(s => s.ToGrpcStatus()); - P.OrchestrationIdReusePolicy? policy = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatusesProto); - - if (policy != null) - { - request.OrchestrationIdReusePolicy = policy; - } + request.OrchestrationIdReusePolicy = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatusesProto); using Activity? newActivity = TraceHelper.StartActivityForNewOrchestration(request); diff --git a/test/Client/Grpc.Tests/ProtoUtilsTests.cs b/test/Client/Grpc.Tests/ProtoUtilsTests.cs index a8669438..ee571e3a 100644 --- a/test/Client/Grpc.Tests/ProtoUtilsTests.cs +++ b/test/Client/Grpc.Tests/ProtoUtilsTests.cs @@ -29,7 +29,7 @@ public void GetAllStatuses_ReturnsExpectedStatuses() } [Fact] - public void GetTerminalStatuses_ReturnsImmutableArray() + public void GetAllStatuses_ReturnsImmutableArray() { // Act ImmutableArray allStatuses = ProtoUtils.GetAllStatuses(); @@ -40,7 +40,7 @@ public void GetTerminalStatuses_ReturnsImmutableArray() } [Fact] - public void ConvertDedupeStatusesToReusePolicy_EmptyArray_ReturnsPolicyWithAllTerminalStatuses() + public void ConvertDedupeStatusesToReusePolicy_EmptyArray_ReturnsPolicyWithAllStatuses() { // Arrange var dedupeStatuses = Array.Empty(); @@ -55,7 +55,7 @@ public void ConvertDedupeStatusesToReusePolicy_EmptyArray_ReturnsPolicyWithAllTe } [Fact] - public void ConvertDedupeStatusesToReusePolicy_AllStatuses_ReturnsNull() + public void ConvertDedupeStatusesToReusePolicy_AllStatuses_ReturnsPolicyWithNoReplaceableStatuses() { // Arrange ImmutableArray allStatuses = ProtoUtils.GetAllStatuses(); @@ -78,7 +78,7 @@ public void ConvertDedupeStatusesToReusePolicy_NoDedupeStatuses_ReturnsPolicyWit P.OrchestrationIdReusePolicy result = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatuses); // Assert - // When no dedupe statuses, all terminal statuses should be replaceable + // When no dedupe statuses, all statuses should be replaceable result.Should().NotBeNull(); result!.ReplaceableStatus.Should().HaveCount(7); result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Completed); @@ -114,7 +114,6 @@ public void ConvertDedupeStatusesToReusePolicy_SingleDedupeStatus_ReturnsPolicyW result.ReplaceableStatus.Should().Contain(P.OrchestrationStatus.Suspended); result.ReplaceableStatus.Should().NotContain(P.OrchestrationStatus.Running); - } [Fact] @@ -363,7 +362,7 @@ public void ConvertDedupeStatusesToReusePolicy_EmptyArray_ThenConvertBack_Return P.OrchestrationStatus[]? convertedBack = ProtoUtils.ConvertReusePolicyToDedupeStatuses(policy); // Assert - // Empty dedupe statuses -> all terminal statuses are replaceable -> policy with all statuses + // Empty dedupe statuses -> all statuses are replaceable -> policy with all statuses // Policy with all statuses -> no dedupe statuses -> null policy.Should().NotBeNull(); convertedBack.Should().BeNull(); From c54e0cb68c103950b7e2a28c4dbe2745a649f496 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Mon, 26 Jan 2026 12:00:12 -0800 Subject: [PATCH 3/6] updating documentation --- src/Client/Core/DurableTaskClient.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Client/Core/DurableTaskClient.cs b/src/Client/Core/DurableTaskClient.cs index c336c38a..64a7f095 100644 --- a/src/Client/Core/DurableTaskClient.cs +++ b/src/Client/Core/DurableTaskClient.cs @@ -73,10 +73,11 @@ public virtual Task ScheduleNewOrchestrationInstanceAsync( /// /// All orchestrations must have a unique instance ID. You can provide an instance ID using the /// parameter or you can omit this and a random instance ID will be - /// generated for you automatically. If an orchestration with the specified instance ID already exists and is in a - /// non-terminal state (Pending, Running, etc.), then this operation may fail silently. However, if an orchestration - /// instance with this ID already exists in a terminal state (Completed, Terminated, Failed, etc.) then the instance - /// may be recreated automatically, depending on the configuration of the backend instance store. + /// generated for you automatically. If an orchestration with the specified instance ID already exists and its status + /// is not in the field of , then + /// a new orchestration may be recreated automatically, depending on the configuration of the backend instance store. + /// If the existing orchestration is in a non-terminal state (Pending, Running, etc.), then the orchestration will first + /// be terminated before the new orchestration is created. /// /// Orchestration instances started with this method will be created in the /// state and will transition to the @@ -98,8 +99,9 @@ public virtual Task ScheduleNewOrchestrationInstanceAsync( /// /// The options to start the new orchestration with. /// - /// The cancellation token. This only cancels enqueueing the new orchestration to the backend. Does not cancel the - /// orchestration once enqueued. + /// The cancellation token. This only cancels enqueueing the new orchestration to the backend, or waiting for the + /// termination of an existing non-terminal instance if its status is not in + /// . Does not cancel the orchestration once enqueued. /// /// /// A task that completes when the orchestration instance is successfully scheduled. The value of this task is From d8c26b398ec5409c1c6e32eba93e3498b68b31b9 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Mon, 26 Jan 2026 12:19:16 -0800 Subject: [PATCH 4/6] added implementation to shim client too --- .../ShimDurableTaskClient.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs b/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs index f4911098..4051724b 100644 --- a/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs +++ b/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using DurableTask.Core; +using DurableTask.Core.Exceptions; using DurableTask.Core.History; using DurableTask.Core.Query; using Microsoft.DurableTask.Client; @@ -224,6 +225,7 @@ public override async Task ScheduleNewOrchestrationInstanceAsync( .ToArray(); } + await this.TerminateTaskOrchestrationWithReusableRunningStatusAndWaitAsync(instanceId, dedupeStatuses, cancellation); await this.Client.CreateTaskOrchestrationAsync(message, dedupeStatuses); return instanceId; } @@ -385,4 +387,53 @@ Task SendInstanceMessageAsync(string instanceId, HistoryEvent @event, Cancellati return this.Client.SendTaskOrchestrationMessageAsync(message); } + + async Task TerminateTaskOrchestrationWithReusableRunningStatusAndWaitAsync( + string instanceId, + OrchestrationStatus[]? dedupeStatuses, + CancellationToken cancellation) + { + var runningStatuses = new List() + { + OrchestrationStatus.Running, + OrchestrationStatus.Pending, + OrchestrationStatus.Suspended, + }; + + // At least one running status is reusable, so determine if an orchestration already exists with this status and terminate it if so + if (dedupeStatuses == null || runningStatuses.Any(status => !dedupeStatuses.Contains(status))) + { + OrchestrationMetadata? metadata = await this.GetInstancesAsync(instanceId, getInputsAndOutputs: false, cancellation); + + if (metadata != null) + { + OrchestrationStatus orchestestrationStatus = metadata.RuntimeStatus.ConvertToCore(); + if (dedupeStatuses?.Contains(orchestestrationStatus) == true) + { + throw new OrchestrationAlreadyExistsException($"An orchestration with instance ID '{instanceId}' and status " + + $"'{metadata.RuntimeStatus}' already exists"); + } + + if (runningStatuses.Contains(orchestestrationStatus)) + { + // Check for cancellation before attempting to terminate the orchestration + cancellation.ThrowIfCancellationRequested(); + + string terminationReason = $"A new instance creation request has been issued for instance {instanceId} which " + + $"currently has status {metadata.RuntimeStatus}. Since the dedupe statuses of the creation request, " + + $"{(dedupeStatuses == null ? "[]" : string.Join(", ", dedupeStatuses))}, do not contain the orchestration's status, " + + $"the orchestration has been terminated and a new instance with the same instance ID will be created."; + + await this.TerminateInstanceAsync(instanceId, terminationReason, cancellation); + + while (metadata != null && metadata.RuntimeStatus != OrchestrationRuntimeStatus.Terminated) + { + cancellation.ThrowIfCancellationRequested(); + await Task.Delay(TimeSpan.FromSeconds(1), cancellation); + metadata = await this.GetInstancesAsync(instanceId, getInputsAndOutputs: false, cancellation); + } + } + } + } + } } From 64c3ba9b68cca55937b613bd9accbb7239b64d3b Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Mon, 26 Jan 2026 14:26:49 -0800 Subject: [PATCH 5/6] added tests for the shim client --- src/Client/Grpc/ProtoUtils.cs | 12 +- .../ShimDurableTaskClientTests.cs | 114 ++++++++++++++++++ 2 files changed, 123 insertions(+), 3 deletions(-) diff --git a/src/Client/Grpc/ProtoUtils.cs b/src/Client/Grpc/ProtoUtils.cs index 1a83d1f2..77da9120 100644 --- a/src/Client/Grpc/ProtoUtils.cs +++ b/src/Client/Grpc/ProtoUtils.cs @@ -64,7 +64,8 @@ public static P.OrchestrationIdReusePolicy ConvertDedupeStatusesToReusePolicy( /// Converts an OrchestrationIdReusePolicy with replaceable statuses to dedupe statuses /// (statuses that should NOT be replaced). /// - /// The OrchestrationIdReusePolicy containing replaceable statuses. + /// The OrchestrationIdReusePolicy containing replaceable statuses. If this parameter is null, + /// then all statuses are considered replaceable. /// An array of orchestration statuses that should NOT be replaced, or null if all statuses are replaceable. /// /// The policy uses "replaceableStatus" - these are statuses that CAN be replaced. @@ -72,8 +73,13 @@ public static P.OrchestrationIdReusePolicy ConvertDedupeStatusesToReusePolicy( /// So dedupeStatuses = all statuses MINUS replaceableStatus. /// public static P.OrchestrationStatus[]? ConvertReusePolicyToDedupeStatuses( - P.OrchestrationIdReusePolicy policy) - { + P.OrchestrationIdReusePolicy? policy) + { + if (policy == null) + { + return null; + } + ImmutableArray allStatuses = GetAllStatuses(); ImmutableHashSet replaceableStatusSet = policy.ReplaceableStatus.ToImmutableHashSet(); diff --git a/test/Client/OrchestrationServiceClientShim.Tests/ShimDurableTaskClientTests.cs b/test/Client/OrchestrationServiceClientShim.Tests/ShimDurableTaskClientTests.cs index d25e9e81..316076cb 100644 --- a/test/Client/OrchestrationServiceClientShim.Tests/ShimDurableTaskClientTests.cs +++ b/test/Client/OrchestrationServiceClientShim.Tests/ShimDurableTaskClientTests.cs @@ -1,11 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using DotNext; using DurableTask.Core; using DurableTask.Core.Entities; +using DurableTask.Core.Exceptions; using DurableTask.Core.History; using DurableTask.Core.Query; using FluentAssertions.Specialized; +using Microsoft.DurableTask; using Microsoft.DurableTask.Client.Entities; using Microsoft.DurableTask.Converters; using Microsoft.Extensions.Options; @@ -419,6 +422,10 @@ public async Task ScheduleNewOrchestrationInstance_ValidDedupeStatuses_DoesNotTh }; // Setup the mock to handle the call + this.orchestrationClient + .Setup(m => m.GetOrchestrationStateAsync(It.IsAny(), false)) + .ReturnsAsync((List?)null); + this.orchestrationClient.Setup( m => m.CreateTaskOrchestrationAsync( It.IsAny(), @@ -446,6 +453,10 @@ public async Task ScheduleNewOrchestrationInstance_CaseInsensitiveValidStatus_Do }; // Setup the mock to handle the call + this.orchestrationClient + .Setup(m => m.GetOrchestrationStateAsync(It.IsAny(), false)) + .ReturnsAsync((List?)null); + this.orchestrationClient.Setup( m => m.CreateTaskOrchestrationAsync( It.IsAny(), @@ -473,6 +484,10 @@ public async Task ScheduleNewOrchestrationInstance_EmptyDedupeStatuses_DoesNotTh }; // Setup the mock to handle the call + this.orchestrationClient + .Setup(m => m.GetOrchestrationStateAsync(It.IsAny(), false)) + .ReturnsAsync((List?)null); + this.orchestrationClient.Setup( m => m.CreateTaskOrchestrationAsync( It.IsAny(), @@ -500,6 +515,10 @@ public async Task ScheduleNewOrchestrationInstance_NullDedupeStatuses_DoesNotThr }; // Setup the mock to handle the call + this.orchestrationClient + .Setup(m => m.GetOrchestrationStateAsync(It.IsAny(), false)) + .ReturnsAsync((List?)null); + this.orchestrationClient.Setup( m => m.CreateTaskOrchestrationAsync( It.IsAny(), @@ -517,6 +536,97 @@ public async Task ScheduleNewOrchestrationInstance_NullDedupeStatuses_DoesNotThr this.orchestrationClient.VerifyAll(); } + [Fact] + public async Task ScheduleNewOrchestrationInstance_TerminatesExistingRunningOrchestration() + { + // Arrange + string instanceId = "test-instance-id"; + var options = new StartOrchestrationOptions + { + InstanceId = instanceId, + DedupeStatuses = ["Pending", "Failed"] + }; + + // Set up GetOrchestrationStateAsync to return Running first, then Terminated + this.orchestrationClient + .SetupSequence(m => m.GetOrchestrationStateAsync(instanceId, false)) + .ReturnsAsync( + [ + new() + { + OrchestrationInstance = new OrchestrationInstance { InstanceId = instanceId }, + OrchestrationStatus = OrchestrationStatus.Running + } + ]) + .ReturnsAsync( + [ + new() + { + OrchestrationInstance = new OrchestrationInstance { InstanceId = instanceId }, + OrchestrationStatus = OrchestrationStatus.Terminated + } + ]); + + // Set up termination call + this.orchestrationClient + .Setup(m => m.ForceTerminateTaskOrchestrationAsync(instanceId, It.IsAny())) + .Returns(Task.CompletedTask); + + // Set up creation call + this.orchestrationClient + .Setup(m => m.CreateTaskOrchestrationAsync( + It.IsAny(), + It.Is(statuses => + statuses != null && + statuses.Length == 2 && + statuses.Contains(OrchestrationStatus.Pending) && + statuses.Contains(OrchestrationStatus.Failed)))) + .Returns(Task.CompletedTask); + + // Act + await this.client.ScheduleNewOrchestrationInstanceAsync( + new TaskName("TestOrchestration"), + input: null, + options, + CancellationToken.None); + + // Assert + this.orchestrationClient.VerifyAll(); + } + + [Fact] + public async Task ScheduleNewOrchestrationInstance_ThrowsOrchestrationAlreadyExistsException() + { + // Arrange + string instanceId = "test-instance-id"; + var options = new StartOrchestrationOptions + { + InstanceId = instanceId, + DedupeStatuses = ["Pending", "Failed"] + }; + + // Set up GetOrchestrationStateAsync to return a Pending orchestration + this.orchestrationClient + .Setup(m => m.GetOrchestrationStateAsync(instanceId, false)) + .ReturnsAsync( + [ + new() { + OrchestrationInstance = new OrchestrationInstance { InstanceId = instanceId }, + OrchestrationStatus = OrchestrationStatus.Pending + } + ]); + + // Act & Assert + Func act = async () => await this.client.ScheduleNewOrchestrationInstanceAsync( + new TaskName("TestOrchestration"), + input: null, + options, + CancellationToken.None); + + await act.Should().ThrowAsync(); + this.orchestrationClient.VerifyAll(); + } + [Theory] [InlineData(false)] [InlineData(true)] @@ -724,6 +834,10 @@ async Task RunScheduleNewOrchestrationInstanceAsync( TaskName name, object? input, StartOrchestrationOptions? options) { // arrange + this.orchestrationClient + .Setup(m => m.GetOrchestrationStateAsync(It.IsAny(), false)) + .ReturnsAsync((List?)null); + this.orchestrationClient.Setup( m => m.CreateTaskOrchestrationAsync( MatchStartExecutionMessage(name, input, options), From 796f522190eadeadd7185de608998427e12c9ab9 Mon Sep 17 00:00:00 2001 From: Sophia Tevosyan Date: Mon, 26 Jan 2026 14:33:55 -0800 Subject: [PATCH 6/6] PR comments --- src/Client/Grpc/ProtoUtils.cs | 3 +-- .../ShimDurableTaskClient.cs | 8 ++++---- .../ShimDurableTaskClientTests.cs | 3 --- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Client/Grpc/ProtoUtils.cs b/src/Client/Grpc/ProtoUtils.cs index 77da9120..222d5f39 100644 --- a/src/Client/Grpc/ProtoUtils.cs +++ b/src/Client/Grpc/ProtoUtils.cs @@ -19,8 +19,7 @@ public static class ProtoUtils /// An immutable array of all orchestration statuses. public static ImmutableArray GetAllStatuses() { -#pragma warning disable CS0618 // Type or member is obsolete - Canceled is intentionally included for compatibility - // compatibility with what? +#pragma warning disable CS0618 // Type or member is obsolete - Canceled is intentionally included for compatibility return ImmutableArray.Create( P.OrchestrationStatus.Completed, P.OrchestrationStatus.Failed, diff --git a/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs b/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs index 4051724b..c20ec2e0 100644 --- a/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs +++ b/src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs @@ -407,14 +407,14 @@ async Task TerminateTaskOrchestrationWithReusableRunningStatusAndWaitAsync( if (metadata != null) { - OrchestrationStatus orchestestrationStatus = metadata.RuntimeStatus.ConvertToCore(); - if (dedupeStatuses?.Contains(orchestestrationStatus) == true) + OrchestrationStatus orchestrationStatus = metadata.RuntimeStatus.ConvertToCore(); + if (dedupeStatuses?.Contains(orchestrationStatus) == true) { throw new OrchestrationAlreadyExistsException($"An orchestration with instance ID '{instanceId}' and status " + $"'{metadata.RuntimeStatus}' already exists"); } - if (runningStatuses.Contains(orchestestrationStatus)) + if (runningStatuses.Contains(orchestrationStatus)) { // Check for cancellation before attempting to terminate the orchestration cancellation.ThrowIfCancellationRequested(); @@ -426,7 +426,7 @@ async Task TerminateTaskOrchestrationWithReusableRunningStatusAndWaitAsync( await this.TerminateInstanceAsync(instanceId, terminationReason, cancellation); - while (metadata != null && metadata.RuntimeStatus != OrchestrationRuntimeStatus.Terminated) + while (metadata != null && !metadata.IsCompleted) { cancellation.ThrowIfCancellationRequested(); await Task.Delay(TimeSpan.FromSeconds(1), cancellation); diff --git a/test/Client/OrchestrationServiceClientShim.Tests/ShimDurableTaskClientTests.cs b/test/Client/OrchestrationServiceClientShim.Tests/ShimDurableTaskClientTests.cs index 316076cb..8043b7a4 100644 --- a/test/Client/OrchestrationServiceClientShim.Tests/ShimDurableTaskClientTests.cs +++ b/test/Client/OrchestrationServiceClientShim.Tests/ShimDurableTaskClientTests.cs @@ -1,14 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using DotNext; using DurableTask.Core; using DurableTask.Core.Entities; using DurableTask.Core.Exceptions; using DurableTask.Core.History; using DurableTask.Core.Query; -using FluentAssertions.Specialized; -using Microsoft.DurableTask; using Microsoft.DurableTask.Client.Entities; using Microsoft.DurableTask.Converters; using Microsoft.Extensions.Options;