Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
eb8f4c4
first commit, will add e2e tests in the next one
Jan 16, 2026
939f980
Merge branch 'dev' into stevosyan/add-dedupe-statuses
Jan 16, 2026
28bd993
added the e2e tests:
Jan 17, 2026
56c9551
removing unnecessary changes
Jan 17, 2026
9145d44
made hellocities more succinct
Jan 17, 2026
c1a7805
missed instance ID assignment
Jan 17, 2026
7bfde01
PR comment updates
Jan 21, 2026
c22d39f
fixing whitespace
Jan 21, 2026
73e34ba
removed linked cancellation token use
Jan 26, 2026
a90d03e
updated the exception handling in the e2e test to only catch Orchestr…
Jan 26, 2026
5f07dea
Merge branch 'dev' into stevosyan/add-dedupe-statuses
Jan 26, 2026
ca2d617
removing question comments
Jan 26, 2026
ab99f05
removed unnecessary usings
Jan 26, 2026
4aaccc2
fixing a comment
Jan 26, 2026
6d5ee8c
updating the terminating poll to end if the orchestration reaches any…
Jan 26, 2026
ff08413
updated HTTP request to include cancellation token
Jan 27, 2026
ff30220
fixing the build bug
Jan 27, 2026
8b4c17a
fixed CreateTaskOrchestration overrides to ultimately call the same m…
Jan 27, 2026
aad6002
PR comments:
Feb 4, 2026
33d0f63
updating the termination poll logic to use WaitForOrchestration
Feb 5, 2026
ffc19f9
Merge branch 'dev' into stevosyan/add-dedupe-statuses
Feb 5, 2026
0f0f28d
Adding an ArgumentException for invalid dedupe statuses (any running …
Feb 11, 2026
dc29240
added support to terminate existing running instances for restart
Feb 11, 2026
142e172
Merge branch 'dev' into stevosyan/add-dedupe-statuses
Feb 11, 2026
7e53ac6
Update src/WebJobs.Extensions.DurableTask/HttpApiHandler.cs
sophiatev Feb 11, 2026
ac48ed7
Update test/e2e/Tests/Tests/DedupeStatusesTests.cs
sophiatev Feb 11, 2026
25d8932
Update test/e2e/Tests/Tests/RestartOrchestrationTests.cs
sophiatev Feb 11, 2026
b608832
fixing the typoe
Feb 11, 2026
583c22e
added log assertions to the tests, removed the huge RestartOrchestrat…
Feb 11, 2026
d530408
Update test/e2e/Apps/BasicJava/src/main/java/com/function/HelloCities…
sophiatev Feb 11, 2026
2ea3890
fixing the build warnings and addressing PR comments
Feb 20, 2026
4850850
Merge branch 'dev' into stevosyan/add-dedupe-statuses
Feb 20, 2026
cda3ae0
removing the version specified in one of the package references in VS…
Feb 20, 2026
5f1a596
further fixing the nuget build errors
Feb 20, 2026
2c0af1e
continuing the nuget attempts, addressing some copilot comments
Feb 20, 2026
8959fcf
fixed the failing test
Feb 20, 2026
513376d
updated .NET SDK dependencies
Feb 26, 2026
5c76266
Merge branch 'dev' into stevosyan/add-dedupe-statuses
Feb 26, 2026
d478db5
attempting to fix the e2e dts and mssql errors
Feb 27, 2026
fc60b13
Update test/e2e/Apps/BasicPowerShell/LargeOutputOrchestrator/run.ps1
sophiatev Feb 27, 2026
7d3ecfc
moving the placement of the log checks in the restart tests to try to…
Feb 27, 2026
371bc48
added conditional skip on the Suspended status for MSSQL since it doe…
Feb 27, 2026
ef4f298
changing the logic in the restart test to wait for the restart to com…
Feb 27, 2026
da873e5
Update test/e2e/Apps/BasicNode/src/functions/LargeOutputOrchestrator.ts
sophiatev Feb 27, 2026
e630439
more changes to try to fix the flakiness in the tests
Feb 27, 2026
5d54f01
missed a pending case in DedupeStatusesTests
Feb 27, 2026
d25c05c
fixing the ScheduledStartTime typo in the tests
Feb 28, 2026
b9a7453
updated the DTS and MSSQL dependencies which unblocked certain test c…
Mar 2, 2026
2fbc898
Merge branch 'dev' into stevosyan/add-dedupe-statuses
Mar 2, 2026
0bdca81
updated the mssql dependency to the working package
Mar 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 114 additions & 4 deletions src/WebJobs.Extensions.DurableTask/DurabilityProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DurableTask.Core;
using DurableTask.Core.Entities;
using DurableTask.Core.Exceptions;
using DurableTask.Core.History;
using DurableTask.Core.Query;
using Microsoft.Azure.WebJobs.Host.Scale;
Expand Down Expand Up @@ -106,7 +108,13 @@ public DurabilityProvider(string storageProviderName, IOrchestrationService serv
/// <summary>
/// Event source name (e.g. DurableTask-AzureStorage).
/// </summary>
public virtual string EventSourceName { get; set; }
public virtual string EventSourceName { get; set; }

/// <summary>
/// Gets or sets the amount of time in seconds before a creation request for an orchestration times out.
/// Default value is 180 seconds.
/// </summary>
internal int OrchestrationCreationRequestTimeoutInSeconds { get; set; } = 180;

/// <inheritdoc/>
public int TaskOrchestrationDispatcherCount => this.GetOrchestrationService().TaskOrchestrationDispatcherCount;
Expand Down Expand Up @@ -407,9 +415,45 @@ public Task CreateTaskOrchestrationAsync(TaskMessage creationMessage)
}

/// <inheritdoc />
public Task CreateTaskOrchestrationAsync(TaskMessage creationMessage, OrchestrationStatus[] dedupeStatuses)
{
return this.GetOrchestrationServiceClient().CreateTaskOrchestrationAsync(creationMessage, dedupeStatuses);
/// should this method comment be updated too? to mention terminating existing instances?
Comment thread
sophiatev marked this conversation as resolved.
Outdated
Comment thread
sophiatev marked this conversation as resolved.
Outdated
Comment thread
sophiatev marked this conversation as resolved.
Outdated
public async virtual Task CreateTaskOrchestrationAsync(TaskMessage creationMessage, OrchestrationStatus[] dedupeStatuses)
{
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(this.OrchestrationCreationRequestTimeoutInSeconds));
await this.TerminateTaskOrchestrationWithReusableRunningStatusAndWaitAsync(
creationMessage.OrchestrationInstance.InstanceId,
dedupeStatuses,
timeoutCts.Token);
await this.GetOrchestrationServiceClient().CreateTaskOrchestrationAsync(creationMessage, dedupeStatuses);
}

/// <summary>
/// Creates a new task orchestration instance using the specified creation message and dedupe statuses.
/// </summary>
/// <param name="creationMessage">The creation message for the orchestration.</param>
/// <param name="dedupeStatuses">An array of orchestration statuses used for "dedupping":
Comment thread
sophiatev marked this conversation as resolved.
/// If an orchestration with the same instance ID already exists, and its status is in this array, then a
/// <see cref="OrchestrationAlreadyExistsException"/> will be thrown.
/// If the array contains all of the running statuses (<see cref="OrchestrationStatus.Pending"/>, <see cref="OrchestrationStatus.Running"/>,
/// and <see cref="OrchestrationStatus.Suspended"/>), then only terminal statuses can be reused.
Comment thread
sophiatev marked this conversation as resolved.
/// If at least one of these statuses is not included in the array, then if an instance with that status is found, it will first be terminated
/// before a new orchestration is created.</param>
Comment thread
sophiatev marked this conversation as resolved.
Outdated
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that completes when the creation message for the task orchestration instance is enqueued.</returns>
/// <exception cref="OrchestrationAlreadyExistsException">Thrown if an orchestration with the same instance ID already exists and its status
/// is in <paramref name="dedupeStatuses"/>.</exception>
/// <exception cref="OperationCanceledException">Thrown if the operation is cancelled via <paramref name="cancellationToken"/>.</exception>
public async Task CreateTaskOrchestrationAsync(TaskMessage creationMessage, OrchestrationStatus[] dedupeStatuses, CancellationToken cancellationToken)
{
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(this.OrchestrationCreationRequestTimeoutInSeconds));
using var linkedCts =
Comment thread
sophiatev marked this conversation as resolved.
Outdated
CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken,
timeoutCts.Token);
await this.TerminateTaskOrchestrationWithReusableRunningStatusAndWaitAsync(
creationMessage.OrchestrationInstance.InstanceId,
dedupeStatuses,
linkedCts.Token);
await this.GetOrchestrationServiceClient().CreateTaskOrchestrationAsync(creationMessage, dedupeStatuses);
}

/// <inheritdoc />
Expand Down Expand Up @@ -609,6 +653,72 @@ public virtual bool TryGetTargetScaler(
public virtual Task<IAsyncEnumerable<HistoryEvent>> StreamOrchestrationHistoryAsync(string instanceId, CancellationToken cancellationToken)
{
throw this.GetNotImplementedException(nameof(this.StreamOrchestrationHistoryAsync));
}

/// <summary>
/// If an orchestration exists with a status that is not in <paramref name="dedupeStatuses"/> and has a running status (one of
/// <see cref="OrchestrationStatus.Pending"/>, <see cref="OrchestrationStatus.Running"/>, or <see cref="OrchestrationStatus.Suspended"/>),
Comment thread
sophiatev marked this conversation as resolved.
/// then this method terminates the specified orchestration instance and waits until:
/// - The orchestration's status changes to <see cref="OrchestrationStatus.Terminated"/>,
/// - or the orchestration is deleted,
/// - or the operation is cancelled via the <paramref name="cancellationToken"/>.
/// </summary>
/// <param name="instanceId">The instance ID of the orchestration.</param>
/// <param name="dedupeStatuses">The dedupe statuses of the orchestration.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that completes when any of the above conditions are reached.</returns>
/// <exception cref="OperationCanceledException">Thrown if the operation is cancelled via the <paramref name="cancellationToken"/>.</exception>
/// <exception cref="OrchestrationAlreadyExistsException">Thrown if an orchestration already exists with status in <paramref name="dedupeStatuses"/>.</exception>
private async Task TerminateTaskOrchestrationWithReusableRunningStatusAndWaitAsync(
string instanceId,
OrchestrationStatus[] dedupeStatuses,
CancellationToken cancellationToken)
{
var runningStatuses = new List<OrchestrationStatus>()
{
OrchestrationStatus.Running,
OrchestrationStatus.Pending,
OrchestrationStatus.Suspended,
Comment thread
sophiatev marked this conversation as resolved.
Comment thread
sophiatev marked this conversation as resolved.
};

// At least one running status is reusable, so determine if an orchestration already exists with this status and terminate it if so
if (runningStatuses.Any(status => !dedupeStatuses.Contains(status)))
{
OrchestrationState orchestrationState = await this.GetOrchestrationStateAsync(instanceId, executionId: null);

if (orchestrationState != null)
{
if (dedupeStatuses.Contains(orchestrationState.OrchestrationStatus))
{
throw new OrchestrationAlreadyExistsException($"An orchestration with instance ID '{instanceId}' and status " +
$"'{orchestrationState.OrchestrationStatus}' already exists");
}

if (runningStatuses.Contains(orchestrationState.OrchestrationStatus))
{
// Check for cancellation before attempting to terminate the orchestration
cancellationToken.ThrowIfCancellationRequested();

await this.ForceTerminateTaskOrchestrationAsync(
instanceId,
$"A new instance creation request has been issued for instance {instanceId} which currently has status " +
$"{orchestrationState.OrchestrationStatus}. Since the dedupe statuses of the creation request, " +
$"{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.");
Comment thread
sophiatev marked this conversation as resolved.
Outdated

while (orchestrationState != null && orchestrationState.OrchestrationStatus != OrchestrationStatus.Terminated)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
orchestrationState = await this.GetOrchestrationStateAsync(instanceId, executionId: null);
Comment thread
sophiatev marked this conversation as resolved.
Outdated
}
Comment thread
sophiatev marked this conversation as resolved.
Outdated

Comment thread
sophiatev marked this conversation as resolved.
Outdated
// What should we do here? If dedupe statuses contains terminated, then the creation call afterwards will fail.
// Or should we throw an invalid argument exception if dedupeStatuses contains terminated but also allows for reuse of a running status?
// dedupeStatuses = dedupeStatuses.Except(new List<OrchestrationStatus>() { OrchestrationStatus.Terminated }).ToArray();
Comment thread
sophiatev marked this conversation as resolved.
Outdated
}
}
}
}
}
}
2 changes: 2 additions & 0 deletions src/WebJobs.Extensions.DurableTask/DurableTaskExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ public DurableTaskExtension(
{
this.OutOfProcProtocol = OutOfProcOrchestrationProtocol.OrchestratorShim;
}

this.defaultDurabilityProvider.OrchestrationCreationRequestTimeoutInSeconds = this.Options.OrchestrationCreationRequestTimeoutInSeconds;
}

internal DurableTaskOptions Options { get; }
Expand Down
12 changes: 12 additions & 0 deletions src/WebJobs.Extensions.DurableTask/HttpApiHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Threading;
using System.Threading.Tasks;
using DurableTask.Core;
using DurableTask.Core.Exceptions;
using DurableTask.Core.History;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Template;
Expand Down Expand Up @@ -968,6 +969,17 @@ await durableClient.DurabilityProvider.CreateTaskOrchestrationAsync(
{
return request.CreateErrorResponse(HttpStatusCode.BadRequest, "Invalid JSON content", e);
}
catch (OrchestrationAlreadyExistsException e)
{
return request.CreateErrorResponse(HttpStatusCode.Conflict, e.Message);
}
catch (OperationCanceledException)
{
return request.CreateErrorResponse(
HttpStatusCode.RequestTimeout,
$"Create instance request exceeded timeout of {this.durableTaskOptions.OrchestrationCreationRequestTimeoutInSeconds} " +
$"seconds for instance ID {instanceId} while waiting for the termination of the existing instance with this instance ID.");
}
}

private static string GetHeaderValueFromHeaders(string header, HttpRequestHeaders headers)
Expand Down
25 changes: 24 additions & 1 deletion src/WebJobs.Extensions.DurableTask/Options/DurableTaskOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.ComponentModel;
using System.Net.Http;
using DurableTask.AzureStorage.Partitioning;
using DurableTask.Core;
using DurableTask.Core.Settings;
using Microsoft.Azure.WebJobs.Extensions.DurableTask.Grpc;
using Microsoft.Azure.WebJobs.Host;
Expand Down Expand Up @@ -270,7 +271,22 @@ public string HubName
/// Default is 100 seconds.
/// This settings only applies when .NET 6 or greater is used.
/// </summary>
public TimeSpan? GrpcHttpClientTimeout { get; set; } = TimeSpan.FromSeconds(100);
public TimeSpan? GrpcHttpClientTimeout { get; set; } = TimeSpan.FromSeconds(100);

/// <summary>
/// Gets or sets the amount of time in seconds before a creation request for an orchestration times out.
/// Default value is 180 seconds.
/// </summary>
/// <remarks>
/// This setting is applicable when <see cref="OverridableExistingInstanceStates"/> is set to <see cref="OverridableStates.AnyState"/>.
/// If an orchestration in a non-terminal state already exists with the instance ID passed to the creation request, then this
/// orchestration will be terminated before the new orchestration is created. This setting controls how long the extension will wait
/// for the orchestration to reach a status of <see cref="OrchestrationStatus.Terminated"/> before failing the creation request.
/// </remarks>
/// <value>
/// The number of seconds before a creation request for an orchestration times out.
/// </value>
public int OrchestrationCreationRequestTimeoutInSeconds { get; set; } = 180;
Comment thread
sophiatev marked this conversation as resolved.
Outdated

/// <summary>
/// Gets or sets the local gRPC listener mode, controlling what version of gRPC listener is created.
Expand Down Expand Up @@ -388,6 +404,13 @@ internal void Validate(INameResolver environmentVariableResolver)
{
throw new InvalidOperationException($"{nameof(this.MaxEntityOperationBatchSize)} must be a positive integer value.");
}

if (this.OrchestrationCreationRequestTimeoutInSeconds <= 0 || this.OrchestrationCreationRequestTimeoutInSeconds >= 230)
{
throw new InvalidOperationException($"{nameof(this.OrchestrationCreationRequestTimeoutInSeconds)} must be a positive integer value less than 230 seconds," +
$"which is the maximum amount of time that an HTTP triggered Function can take to respond to a request." +
$"See https://docs.azure.cn/en-us//azure-functions/functions-scale#timeout");
Comment thread
sophiatev marked this conversation as resolved.
Outdated
Comment thread
sophiatev marked this conversation as resolved.
Outdated
}
Comment thread
sophiatev marked this conversation as resolved.
Outdated
}

internal bool IsDefaultHubName()
Expand Down
40 changes: 33 additions & 7 deletions src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,28 @@ public override Task<Empty> Hello(Empty request, ServerCallContext context)
{
try
{
var allStatuses = new List<OrchestrationStatus>()
Comment thread
sophiatev marked this conversation as resolved.
Outdated
{
OrchestrationStatus.Running,
OrchestrationStatus.Pending,
OrchestrationStatus.Suspended,
OrchestrationStatus.Completed,
OrchestrationStatus.Failed,
OrchestrationStatus.Terminated,
};
Comment thread
sophiatev marked this conversation as resolved.
Outdated

// Not all clients are necessarily configured to set the OrchestrationIdReusePolicy field of the request.
// If it is null, we assume that they do not support per-request-dedupe statuses, and default to using just
// the OverridableExistingInstanceStates setting instead.
List<OrchestrationStatus> reusableStatuses = request.OrchestrationIdReusePolicy is null
? allStatuses
: request.OrchestrationIdReusePolicy.ReplaceableStatus.Select(status => (OrchestrationStatus)status).ToList();

OrchestrationStatus[] dedupeStatuses = allStatuses
.Except(reusableStatuses)
.Union(this.extension.Options.OverridableExistingInstanceStates.ToDedupeStatuses())
.ToArray();
Comment thread
sophiatev marked this conversation as resolved.

// Create the orchestration instance
var instance = new OrchestrationInstance
{
Expand Down Expand Up @@ -86,7 +108,8 @@ await this.GetDurabilityProvider(context).CreateTaskOrchestrationAsync(
Event = executionStartedEvent,
OrchestrationInstance = instance,
},
this.GetStatusesNotToOverride());
dedupeStatuses,
context.CancellationToken);
Comment thread
sophiatev marked this conversation as resolved.

return new P.CreateInstanceResponse
{
Expand All @@ -100,6 +123,15 @@ await this.GetDurabilityProvider(context).CreateTaskOrchestrationAsync(
catch (InvalidOperationException ex) when (ex.Message.EndsWith("already exists.")) // for older versions of DTF.AS and DTFx.Netherite
{
throw new RpcException(new Status(StatusCode.AlreadyExists, $"An Orchestration instance with the ID {request.InstanceId} already exists."));
}
catch (OperationCanceledException)
{
throw new RpcException(new Status(
StatusCode.Cancelled,
context.CancellationToken.IsCancellationRequested
? $"Create instance request cancelled for instance ID {request.InstanceId}"
: $"Create instance request exceeded timeout of {this.extension.Options.OrchestrationCreationRequestTimeoutInSeconds} seconds " +
$"for instance ID {request.InstanceId} while waiting for the termination of the existing instance with this instance ID."));
}
catch (Exception ex)
{
Expand All @@ -112,12 +144,6 @@ await this.GetDurabilityProvider(context).CreateTaskOrchestrationAsync(
}
}

private OrchestrationStatus[] GetStatusesNotToOverride()
{
OverridableStates overridableStates = this.extension.Options.OverridableExistingInstanceStates;
return overridableStates.ToDedupeStatuses();
}

public async override Task<P.RaiseEventResponse> RaiseEvent(P.RaiseEventRequest request, ServerCallContext context)
{
bool throwStatusExceptionsOnRaiseEvent = this.extension.Options.ThrowStatusExceptionsOnRaiseEvent ?? this.extension.DefaultDurabilityProvider.CheckStatusBeforeRaiseEvent;
Expand Down
Loading
Loading