Skip to content

Commit 1c4d8c9

Browse files
sophiatevSophia Tevosyan
andauthored
Changing the default dedupe statuses behavior (#622)
* first commit? * PR comments * updating documentation * added implementation to shim client too * added tests for the shim client * PR comments * reverted the logic to not include a reuse policy in the case that dedupe statuses is null (never set by the client. this is different than explicitly making it an empty list) * updating the tests accordingly * PR comments * addressing PR comments * updated tests to check for new exception type * slight comment update * Adding an ArgumentException for invalid dedupe statuses (any running + terminated) * added support to terminate existing running instances for restart * addressing copilot comments * addressing the PR comments and build warnings * fixed the build errors * missed a change in comment type in GrpcDurableTaskClient * missed updating a comment in the tests * returned the catching of the RpcException with status code cancelled, since even if the server no longer throws an exception like this gRPC can still throw this under the hood --------- Co-authored-by: Sophia Tevosyan <stevosyan@microsoft.com>
1 parent 2dc5843 commit 1c4d8c9

10 files changed

Lines changed: 531 additions & 219 deletions

File tree

src/Client/Core/DurableTaskClient.cs

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using System.ComponentModel;
5+
using DurableTask.Core.Exceptions;
56
using DurableTask.Core.History;
67
using Microsoft.DurableTask.Client.Entities;
78
using Microsoft.DurableTask.Internal;
@@ -73,10 +74,14 @@ public virtual Task<string> ScheduleNewOrchestrationInstanceAsync(
7374
/// <remarks>
7475
/// <para>All orchestrations must have a unique instance ID. You can provide an instance ID using the
7576
/// <paramref name="options"/> parameter or you can omit this and a random instance ID will be
76-
/// generated for you automatically. If an orchestration with the specified instance ID already exists and is in a
77-
/// non-terminal state (Pending, Running, etc.), then this operation may fail silently. However, if an orchestration
78-
/// instance with this ID already exists in a terminal state (Completed, Terminated, Failed, etc.) then the instance
79-
/// may be recreated automatically, depending on the configuration of the backend instance store.
77+
/// generated for you automatically. If an orchestration with the specified instance ID already exists and its status
78+
/// is not in the <see cref="StartOrchestrationOptions.DedupeStatuses"/> field of <paramref name="options"/>, then
79+
/// a new orchestration may be recreated automatically, depending on the configuration of the backend instance store.
80+
/// If the existing orchestration is in a non-terminal state (Pending, Running, etc.), then the orchestration will first
81+
/// be terminated before the new orchestration is created.
82+
/// A null <see cref="StartOrchestrationOptions.DedupeStatuses"/> field means the deduplication behavior will follow
83+
/// whatever the default deduplication behavior of the backend instance store or implementation of this client is.
84+
/// A non-null, empty field means that all statuses are reusable.
8085
/// </para><para>
8186
/// Orchestration instances started with this method will be created in the
8287
/// <see cref="OrchestrationRuntimeStatus.Pending"/> state and will transition to the
@@ -98,15 +103,24 @@ public virtual Task<string> ScheduleNewOrchestrationInstanceAsync(
98103
/// </param>
99104
/// <param name="options">The options to start the new orchestration with.</param>
100105
/// <param name="cancellation">
101-
/// The cancellation token. This only cancels enqueueing the new orchestration to the backend. Does not cancel the
102-
/// orchestration once enqueued.
106+
/// The cancellation token. This only cancels enqueueing the new orchestration to the backend, or waiting for the
107+
/// termination of an existing non-terminal instance if its status is not in
108+
/// <see cref="StartOrchestrationOptions.DedupeStatuses"/>. Does not cancel the orchestration once enqueued.
103109
/// </param>
104110
/// <returns>
105111
/// A task that completes when the orchestration instance is successfully scheduled. The value of this task is
106112
/// the instance ID of the scheduled orchestration instance. If a non-null instance ID was provided via
107113
/// <paramref name="options" />, the same value will be returned by the completed task.
108114
/// </returns>
109115
/// <exception cref="ArgumentNullException">Thrown if <paramref name="orchestratorName"/> is empty.</exception>
116+
/// <exception cref="OrchestrationAlreadyExistsException">If an orchestration with status in
117+
/// <see cref="StartOrchestrationOptions.DedupeStatuses"/> with this instance ID already exists.</exception>
118+
/// <exception cref="ArgumentException">
119+
/// Thrown if <see cref="StartOrchestrationOptions.DedupeStatuses"/> contains 'Terminated', but also allows at
120+
/// least one running status to be reusable. In this case, an existing orchestration with that running status
121+
/// would be terminated, but the creation of the new orchestration would immediately fail due to the existing
122+
/// orchestration now having status 'Terminated'.
123+
/// </exception>
110124
public abstract Task<string> ScheduleNewOrchestrationInstanceAsync(
111125
TaskName orchestratorName,
112126
object? input = null,
@@ -412,6 +426,9 @@ public virtual Task<PurgeResult> PurgeAllInstancesAsync(
412426
/// <para>
413427
/// This method restarts an existing orchestration instance. If <paramref name="restartWithNewInstanceId"/> is <c>true</c>,
414428
/// a new instance ID will be generated for the restarted orchestration. If <c>false</c>, the original instance ID will be reused.
429+
/// If the existing instance is not in a terminal state, depending on the specific implementation, either:
430+
/// 1. The existing instance will be terminated before being restarted, or
431+
/// 2. An <see cref="InvalidOperationException"/> will be thrown.
415432
/// </para><para>
416433
/// The restarted orchestration will use the same input data as the original instance. If the original orchestration
417434
/// instance is not found, an <see cref="ArgumentException"/> will be thrown.
@@ -426,8 +443,9 @@ public virtual Task<PurgeResult> PurgeAllInstancesAsync(
426443
/// If <c>false</c>, the original instance ID will be reused.
427444
/// </param>
428445
/// <param name="cancellation">
429-
/// The cancellation token. This only cancels enqueueing the restart request to the backend.
430-
/// Does not abort restarting the orchestration once enqueued.
446+
/// The cancellation token. This only cancels enqueueing the restart request to the backend,
447+
/// or waiting for the termination of an existing non-terminal instance if the implementation supports
448+
/// this behavior. Does not abort restarting the orchestration once enqueued.
431449
/// </param>
432450
/// <returns>
433451
/// A task that completes when the orchestration instance is successfully restarted.
@@ -437,7 +455,8 @@ public virtual Task<PurgeResult> PurgeAllInstancesAsync(
437455
/// Thrown if an orchestration with the specified <paramref name="instanceId"/> was not found. </exception>
438456
/// <exception cref="InvalidOperationException">
439457
/// Thrown when attempting to restart an instance using the same instance Id
440-
/// while the instance has not yet reached a completed or terminal state. </exception>
458+
/// while the instance has not yet reached a completed or terminal state, in the case that
459+
/// the implementation does not support terminating existing non-terminal instances before restarting.</exception>
441460
/// <exception cref="NotSupportedException">
442461
/// Thrown if the backend does not support restart operations. </exception>
443462
public virtual Task<string> RestartAsync(
@@ -529,7 +548,7 @@ public virtual Task<Page<string>> ListInstanceIdsAsync(
529548
throw new NotSupportedException(
530549
$"{this.GetType()} does not support listing orchestration instance IDs filtered by completed time.");
531550
}
532-
551+
533552
// TODO: Create task hub
534553

535554
// TODO: Delete task hub
@@ -539,4 +558,4 @@ public virtual Task<Page<string>> ListInstanceIdsAsync(
539558
/// </summary>
540559
/// <returns>A <see cref="ValueTask"/> that completes when the disposal completes.</returns>
541560
public abstract ValueTask DisposeAsync();
542-
}
561+
}

src/Client/Core/StartOrchestrationOptionsExtensions.cs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,26 @@ namespace Microsoft.DurableTask.Client;
99
/// Extension methods for <see cref="StartOrchestrationOptions"/> to provide type-safe deduplication status configuration.
1010
/// </summary>
1111
public static class StartOrchestrationOptionsExtensions
12-
{
13-
public static readonly OrchestrationRuntimeStatus[] ValidDedupeStatuses = new[]
14-
{
12+
{
13+
#pragma warning disable CS0618 // Type or member is obsolete - Cancelled is intentionally included for compatibility with the
14+
// Durable Task Framework
15+
16+
/// <summary>
17+
/// The list of orchestration statuses that can be deduplicated upon a creation request.
18+
/// If one of these statuses is included in the request via the <see cref="StartOrchestrationOptions.DedupeStatuses"/>
19+
/// field, and an orchestration with this status and same instance ID is found, the request will fail.
20+
/// </summary>
21+
public static readonly IReadOnlyList<OrchestrationRuntimeStatus> ValidDedupeStatuses =
22+
[
1523
OrchestrationRuntimeStatus.Completed,
1624
OrchestrationRuntimeStatus.Failed,
17-
OrchestrationRuntimeStatus.Terminated,
25+
OrchestrationRuntimeStatus.Terminated,
1826
OrchestrationRuntimeStatus.Canceled,
19-
};
27+
OrchestrationRuntimeStatus.Pending,
28+
OrchestrationRuntimeStatus.Running,
29+
OrchestrationRuntimeStatus.Suspended,
30+
];
31+
#pragma warning restore CS0618 // Type or member is obsolete
2032

2133
/// <summary>
2234
/// Creates a new <see cref="StartOrchestrationOptions"/> with the specified deduplication statuses.

src/Client/Grpc/GrpcDurableTaskClient.cs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Collections.Immutable;
55
using System.Diagnostics;
66
using System.Text;
7+
using DurableTask.Core.Exceptions;
78
using DurableTask.Core.History;
89
using Google.Protobuf.WellKnownTypes;
910
using Microsoft.DurableTask.Client.Entities;
@@ -73,6 +74,7 @@ public override ValueTask DisposeAsync()
7374
}
7475

7576
/// <inheritdoc/>
77+
// The behavior of this method when the dedupe statuses field is null depends on the server-side implementation.
7678
public override async Task<string> ScheduleNewOrchestrationInstanceAsync(
7779
TaskName orchestratorName,
7880
object? input = null,
@@ -124,9 +126,7 @@ public override async Task<string> ScheduleNewOrchestrationInstanceAsync(
124126
}
125127

126128
// Set orchestration ID reuse policy for deduplication support
127-
// Note: This requires the protobuf to support OrchestrationIdReusePolicy field
128-
// If the protobuf doesn't support it yet, this will need to be updated when the protobuf is updated
129-
if (options?.DedupeStatuses != null && options.DedupeStatuses.Count > 0)
129+
if (options?.DedupeStatuses != null)
130130
{
131131
// Parse and validate all status strings to enum first
132132
ImmutableHashSet<OrchestrationRuntimeStatus> dedupeStatuses = options.DedupeStatuses
@@ -143,19 +143,30 @@ public override async Task<string> ScheduleNewOrchestrationInstanceAsync(
143143

144144
// Convert dedupe statuses to protobuf statuses and create reuse policy
145145
IEnumerable<P.OrchestrationStatus> dedupeStatusesProto = dedupeStatuses.Select(s => s.ToGrpcStatus());
146-
P.OrchestrationIdReusePolicy? policy = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatusesProto);
147-
148-
if (policy != null)
149-
{
150-
request.OrchestrationIdReusePolicy = policy;
151-
}
146+
request.OrchestrationIdReusePolicy = ProtoUtils.ConvertDedupeStatusesToReusePolicy(dedupeStatusesProto);
152147
}
153148

154149
using Activity? newActivity = TraceHelper.StartActivityForNewOrchestration(request);
155150

156-
P.CreateInstanceResponse? result = await this.sidecarClient.StartInstanceAsync(
151+
try
152+
{
153+
P.CreateInstanceResponse? result = await this.sidecarClient.StartInstanceAsync(
157154
request, cancellationToken: cancellation);
158-
return result.InstanceId;
155+
return result.InstanceId;
156+
}
157+
catch (RpcException e) when (e.StatusCode == StatusCode.AlreadyExists)
158+
{
159+
throw new OrchestrationAlreadyExistsException(e.Status.Detail);
160+
}
161+
catch (RpcException e) when (e.StatusCode == StatusCode.InvalidArgument)
162+
{
163+
throw new ArgumentException(e.Status.Detail);
164+
}
165+
catch (RpcException e) when (e.StatusCode == StatusCode.Cancelled)
166+
{
167+
throw new OperationCanceledException(
168+
$"The {nameof(this.ScheduleNewOrchestrationInstanceAsync)} operation was canceled.", e, cancellation);
169+
}
159170
}
160171

161172
/// <inheritdoc/>
@@ -479,6 +490,9 @@ public override Task<PurgeResult> PurgeAllInstancesAsync(
479490
}
480491

481492
/// <inheritdoc/>
493+
// Whether or not this method throws a <see cref="InvalidOperationException"/> or terminates the existing instance
494+
// when <paramref name="restartWithNewInstanceId"/> is <c>false</c> and the existing instance is not in a terminal state
495+
// depends on the server-side implementation.
482496
public override async Task<string> RestartAsync(
483497
string instanceId,
484498
bool restartWithNewInstanceId = false,

src/Client/Grpc/ProtoUtils.cs

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,21 @@ namespace Microsoft.DurableTask.Client.Grpc;
1313
public static class ProtoUtils
1414
{
1515
/// <summary>
16-
/// Gets the terminal orchestration statuses that are commonly used for deduplication.
16+
/// Gets an array of all orchestration statuses.
1717
/// These are the statuses that can be used in OrchestrationIdReusePolicy.
1818
/// </summary>
19-
/// <returns>An immutable array of terminal orchestration statuses.</returns>
20-
public static ImmutableArray<P.OrchestrationStatus> GetTerminalStatuses()
19+
/// <returns>An immutable array of all orchestration statuses.</returns>
20+
public static ImmutableArray<P.OrchestrationStatus> GetAllStatuses()
2121
{
2222
#pragma warning disable CS0618 // Type or member is obsolete - Canceled is intentionally included for compatibility
2323
return ImmutableArray.Create(
2424
P.OrchestrationStatus.Completed,
2525
P.OrchestrationStatus.Failed,
26-
P.OrchestrationStatus.Terminated,
27-
P.OrchestrationStatus.Canceled);
26+
P.OrchestrationStatus.Terminated,
27+
P.OrchestrationStatus.Canceled,
28+
P.OrchestrationStatus.Pending,
29+
P.OrchestrationStatus.Running,
30+
P.OrchestrationStatus.Suspended);
2831
#pragma warning restore CS0618
2932
}
3033

@@ -33,59 +36,60 @@ public static class ProtoUtils
3336
/// with replaceable statuses (statuses that CAN be replaced).
3437
/// </summary>
3538
/// <param name="dedupeStatuses">The orchestration statuses that should NOT be replaced. These are statuses for which an exception should be thrown if an orchestration already exists.</param>
36-
/// <returns>An OrchestrationIdReusePolicy with replaceable statuses set, or null if all terminal statuses are dedupe statuses.</returns>
39+
/// <returns>An OrchestrationIdReusePolicy with replaceable statuses set.</returns>
3740
/// <remarks>
3841
/// The policy uses "replaceableStatus" - these are statuses that CAN be replaced.
3942
/// dedupeStatuses are statuses that should NOT be replaced.
40-
/// So replaceableStatus = all terminal statuses MINUS dedupeStatuses.
43+
/// So replaceableStatus = all statuses MINUS dedupeStatuses.
4144
/// </remarks>
42-
public static P.OrchestrationIdReusePolicy? ConvertDedupeStatusesToReusePolicy(
43-
IEnumerable<P.OrchestrationStatus>? dedupeStatuses)
44-
{
45-
ImmutableArray<P.OrchestrationStatus> terminalStatuses = GetTerminalStatuses();
46-
ImmutableHashSet<P.OrchestrationStatus> dedupeStatusSet = dedupeStatuses?.ToImmutableHashSet() ?? ImmutableHashSet<P.OrchestrationStatus>.Empty;
45+
public static P.OrchestrationIdReusePolicy ConvertDedupeStatusesToReusePolicy(
46+
IEnumerable<P.OrchestrationStatus> dedupeStatuses)
47+
{
48+
Check.NotNull(dedupeStatuses);
49+
ImmutableArray<P.OrchestrationStatus> statuses = GetAllStatuses();
50+
ImmutableHashSet<P.OrchestrationStatus> dedupeStatusSet = [.. dedupeStatuses];
4751

4852
P.OrchestrationIdReusePolicy policy = new();
4953

50-
// Add terminal statuses that are NOT in dedupeStatuses as replaceable
51-
foreach (P.OrchestrationStatus terminalStatus in terminalStatuses.Where(status => !dedupeStatusSet.Contains(status)))
54+
// Add statuses that are NOT in dedupeStatuses as replaceable
55+
foreach (P.OrchestrationStatus status in statuses.Where(status => !dedupeStatusSet.Contains(status)))
5256
{
53-
policy.ReplaceableStatus.Add(terminalStatus);
57+
policy.ReplaceableStatus.Add(status);
5458
}
5559

56-
// Only return policy if we have replaceable statuses
57-
return policy.ReplaceableStatus.Count > 0 ? policy : null;
60+
return policy;
5861
}
5962

6063
/// <summary>
6164
/// Converts an OrchestrationIdReusePolicy with replaceable statuses to dedupe statuses
6265
/// (statuses that should NOT be replaced).
6366
/// </summary>
64-
/// <param name="policy">The OrchestrationIdReusePolicy containing replaceable statuses.</param>
65-
/// <returns>An array of orchestration statuses that should NOT be replaced, or null if all terminal statuses are replaceable.</returns>
67+
/// <param name="policy">The OrchestrationIdReusePolicy containing replaceable statuses. If this parameter is null,
68+
/// then null is returned.</param>
69+
/// <returns>An array of orchestration statuses that should NOT be replaced, which is empty if all statuses
70+
/// are replaceable.</returns>
6671
/// <remarks>
6772
/// The policy uses "replaceableStatus" - these are statuses that CAN be replaced.
6873
/// dedupeStatuses are statuses that should NOT be replaced (should throw exception).
69-
/// So dedupeStatuses = all terminal statuses MINUS replaceableStatus.
74+
/// So dedupeStatuses = all statuses MINUS replaceableStatus.
7075
/// </remarks>
7176
public static P.OrchestrationStatus[]? ConvertReusePolicyToDedupeStatuses(
7277
P.OrchestrationIdReusePolicy? policy)
73-
{
74-
if (policy == null || policy.ReplaceableStatus.Count == 0)
78+
{
79+
if (policy == null)
7580
{
7681
return null;
77-
}
82+
}
7883

79-
ImmutableArray<P.OrchestrationStatus> terminalStatuses = GetTerminalStatuses();
84+
ImmutableArray<P.OrchestrationStatus> allStatuses = GetAllStatuses();
8085
ImmutableHashSet<P.OrchestrationStatus> replaceableStatusSet = policy.ReplaceableStatus.ToImmutableHashSet();
8186

82-
// Calculate dedupe statuses = terminal statuses - replaceable statuses
83-
P.OrchestrationStatus[] dedupeStatuses = terminalStatuses
84-
.Where(terminalStatus => !replaceableStatusSet.Contains(terminalStatus))
87+
// Calculate dedupe statuses = all statuses - replaceable statuses
88+
P.OrchestrationStatus[] dedupeStatuses = allStatuses
89+
.Where(status => !replaceableStatusSet.Contains(status))
8590
.ToArray();
8691

87-
// Only return if there are dedupe statuses
88-
return dedupeStatuses.Length > 0 ? dedupeStatuses : null;
92+
return dedupeStatuses;
8993
}
9094

9195
#pragma warning disable 0618 // Referencing Obsolete member. This is intention as we are only converting it.

0 commit comments

Comments
 (0)