From fb50ef34abc0923d70b876f9f6ccd719697660f4 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Wed, 2 Jul 2025 13:01:18 -0700 Subject: [PATCH 01/15] initial commit --- .../DurableTaskClientExtensions.cs | 38 ++++++++- .../FunctionsDurableTaskClientTests.cs | 1 + .../RestartOrchestration.cs | 54 +++++++++++++ .../Tests/Tests/RestartOrchestrationTests.cs | 81 +++++++++++++++++++ 4 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs create mode 100644 test/e2e/Tests/Tests/RestartOrchestrationTests.cs diff --git a/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs b/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs index 6c05f6419..fe781879f 100644 --- a/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs +++ b/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Azure.Core.Serialization; using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.DurableTask; using Microsoft.DurableTask.Client; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -259,6 +260,7 @@ static string BuildUrl(string url, params string?[] queryValues) { Id = instanceId, PurgeHistoryDeleteUri = BuildUrl(instanceUrl, commonQueryParameters), + RestartPostUri = BuildUrl($"{instanceUrl}/restart", commonQueryParameters), SendEventPostUri = BuildUrl($"{instanceUrl}/raiseEvent/{{eventName}}", commonQueryParameters), StatusQueryGetUri = BuildUrl(instanceUrl, commonQueryParameters), TerminatePostUri = BuildUrl($"{instanceUrl}/terminate", "reason={{text}}", commonQueryParameters), @@ -273,6 +275,41 @@ private static ObjectSerializer GetObjectSerializer(HttpResponseData response) ?? throw new InvalidOperationException("A serializer is not configured for the worker."); } + /// + /// Restarts an existing orchestration instance with the original input. + /// + /// The . + /// The ID of the orchestration instance to restart. + /// If true, starts with a new instance ID; otherwise, uses the same instance ID. + /// A token that signals if the operation should be canceled. + /// The new instance ID. + public static async Task RestartAsync( + this DurableTaskClient client, + string instanceId, + bool restartWithNewInstanceId = true, + CancellationToken cancellation = default) + { + // Get the status of the existing instance, including input + OrchestrationMetadata? status = await client.GetInstancesAsync(instanceId, getInputsAndOutputs: true, cancellation) + ?? throw new ArgumentException($"An orchestration with the instanceId {instanceId} was not found."); + + // Deserialize the input. + string orchestratorName = status.Name; + string? input = status.SerializedInput != null + ? System.Text.Json.JsonSerializer.Deserialize(status.SerializedInput) + : null; + + if (restartWithNewInstanceId) + { + return await client.ScheduleNewOrchestrationInstanceAsync(orchestratorName, input, null, cancellation); + } + else + { + var options = new StartOrchestrationOptions { InstanceId = instanceId }; + return await client.ScheduleNewOrchestrationInstanceAsync(orchestratorName, input, options, cancellation); + } + } + private static string? GetBaseUrlFromRequest(HttpRequestData request) { // Default to the scheme from the request URL @@ -319,7 +356,6 @@ private static ObjectSerializer GetObjectSerializer(HttpResponseData response) return $"{proto}://{host}"; } - private static string? GetQueryParams(DurableTaskClient client) { return client is FunctionsDurableTaskClient functions ? functions.QueryString : null; diff --git a/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs b/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs index 87e3dde29..546b2a6c0 100644 --- a/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs +++ b/test/Worker.Extensions.DurableTask.Tests/FunctionsDurableTaskClientTests.cs @@ -252,6 +252,7 @@ private static void AssertHttpManagementPayload(HttpManagementPayload payload, s { Assert.Equal(instanceId, payload.Id); Assert.Equal($"{BaseUrl}/instances/{instanceId}", payload.PurgeHistoryDeleteUri); + Assert.Equal($"{BaseUrl}/instances/{instanceId}/restart", payload.RestartPostUri); Assert.Equal($"{BaseUrl}/instances/{instanceId}/raiseEvent/{{eventName}}", payload.SendEventPostUri); Assert.Equal($"{BaseUrl}/instances/{instanceId}", payload.StatusQueryGetUri); Assert.Equal($"{BaseUrl}/instances/{instanceId}/terminate?reason={{{{text}}}}", payload.TerminatePostUri); diff --git a/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs b/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs new file mode 100644 index 000000000..c8e545051 --- /dev/null +++ b/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.DurableTask; +using Microsoft.DurableTask.Client; +using Microsoft.Extensions.Logging; +using System.Net; + +namespace Microsoft.Azure.Durable.Tests.E2E; + +public static class RestartOrchestration +{ + [Function(nameof(RestartOrchestrator))] + public static string RestartOrchestrator( + [OrchestrationTrigger] TaskOrchestrationContext context) + { + string? input = context.GetInput(); + return "Hello " + input; + } + + [Function("RestartOrchestration_HttpStart")] + public static async Task HttpStartRestartOrchestration( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, + [DurableClient] DurableTaskClient client, + FunctionContext executionContext) + { + string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(RestartOrchestrator), input: "World"); + return await client.CreateCheckStatusResponseAsync(req, instanceId); + } + + public class RestartRequest + { + public string InstanceId { get; set; } = string.Empty; + public bool RestartWithNewInstanceId { get; set; } + } + + [Function("RestartOrchestration_HttpRestart")] + public static async Task HttpRestartOrchestration( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, + [DurableClient] DurableTaskClient client, + FunctionContext executionContext) + { + var data = await req.ReadFromJsonAsync(); + if (data == null) + { + return req.CreateResponse(HttpStatusCode.BadRequest); + } + string newInstanceId = await client.RestartAsync(data.InstanceId,data.RestartWithNewInstanceId); + + return await client.CreateCheckStatusResponseAsync(req, newInstanceId); + } +} \ No newline at end of file diff --git a/test/e2e/Tests/Tests/RestartOrchestrationTests.cs b/test/e2e/Tests/Tests/RestartOrchestrationTests.cs new file mode 100644 index 000000000..5c8c49798 --- /dev/null +++ b/test/e2e/Tests/Tests/RestartOrchestrationTests.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Net; +using Xunit; +using Xunit.Abstractions; +using System.Text.Json; + +namespace Microsoft.Azure.Durable.Tests.DotnetIsolatedE2E; + +[Collection(Constants.FunctionAppCollectionName)] +public class RestartOrchestrationTests +{ + private readonly FunctionAppFixture fixture; + private readonly ITestOutputHelper output; + + public RestartOrchestrationTests(FunctionAppFixture fixture, ITestOutputHelper testOutputHelper) + { + this.fixture = fixture; + this.fixture.TestLogs.UseTestLogger(testOutputHelper); + this.output = testOutputHelper; + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + // Test behavior of restartasync of durabletaskclient. + // when restart with a instanceid and startwithnewinstanceid is false, the orchestration should be restarted with the same instance id. + // and the output should be the same as the original orchestration. + // when restart with a instanceid and startwithnewinstanceid is true, the orchestration should be restarted with a new instance id. + // and the output should be same as the original orchestration. + public async Task RestartOrchestration_CreatedTimeAndOutputChange(bool restartWithNewInstanceId) + { + // Start the orchestration + using HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger("RestartOrchestration_HttpStart", ""); + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + string statusQueryGetUri = await DurableHelpers.ParseStatusQueryGetUriAsync(response); + string instanceId = await DurableHelpers.ParseInstanceIdAsync(response); + + await DurableHelpers.WaitForOrchestrationStateAsync(statusQueryGetUri, "Completed", 10); + var orchestrationDetails = await DurableHelpers.GetRunningOrchestrationDetailsAsync(statusQueryGetUri); + string output1 = orchestrationDetails.Output; + DateTime createdTime1 = orchestrationDetails.CreatedTime; + + // best practice to wait for 1 seconds before restarting orchestration to avoid race condition. + await Task.Delay(1000); + + var restartPayload = new { + InstanceId = instanceId, + RestartWithNewInstanceId = restartWithNewInstanceId + }; + + string jsonBody = JsonSerializer.Serialize(restartPayload); + + // Restart the orchestrator with the same instance id) + using HttpResponseMessage restartResponse = await HttpHelpers.InvokeHttpTriggerWithBody( + "RestartOrchestration_HttpRestart", jsonBody, "application/json"); + Assert.Equal(HttpStatusCode.Accepted, restartResponse.StatusCode); + string restartStatusQueryGetUri = await DurableHelpers.ParseStatusQueryGetUriAsync(restartResponse); + string restartInstanceId = await DurableHelpers.ParseInstanceIdAsync(restartResponse); + + await DurableHelpers.WaitForOrchestrationStateAsync(restartStatusQueryGetUri, "Completed", 10); + var restartOrchestrationDetails = await DurableHelpers.GetRunningOrchestrationDetailsAsync(restartStatusQueryGetUri); + string output2 = restartOrchestrationDetails.Output; + DateTime createdTime2 = restartOrchestrationDetails.CreatedTime; + + // The outputs and created times should be different + Assert.Equal(output1, output2); + Assert.NotEqual(createdTime1, createdTime2); + + if (restartWithNewInstanceId) + { + // If restartWithNewInstanceId is True, the two instanceId should be different. + Assert.NotEqual(instanceId, restartInstanceId); + } + else + { + Assert.Equal(instanceId, restartInstanceId); + } + } +} \ No newline at end of file From 23cb32585021da7790673162951a41cce89fa35d Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Wed, 2 Jul 2025 13:04:48 -0700 Subject: [PATCH 02/15] add blnk line --- test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs | 2 +- test/e2e/Tests/Tests/RestartOrchestrationTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs b/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs index c8e545051..46c78f7f8 100644 --- a/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs +++ b/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs @@ -51,4 +51,4 @@ public static async Task HttpRestartOrchestration( return await client.CreateCheckStatusResponseAsync(req, newInstanceId); } -} \ No newline at end of file +} diff --git a/test/e2e/Tests/Tests/RestartOrchestrationTests.cs b/test/e2e/Tests/Tests/RestartOrchestrationTests.cs index 5c8c49798..5efc359b6 100644 --- a/test/e2e/Tests/Tests/RestartOrchestrationTests.cs +++ b/test/e2e/Tests/Tests/RestartOrchestrationTests.cs @@ -78,4 +78,4 @@ public async Task RestartOrchestration_CreatedTimeAndOutputChange(bool restartWi Assert.Equal(instanceId, restartInstanceId); } } -} \ No newline at end of file +} From a1cbbf19774e7fd289404b406ca74e1a1c5cda37 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Wed, 2 Jul 2025 13:06:04 -0700 Subject: [PATCH 03/15] sort usings --- test/e2e/Tests/Tests/RestartOrchestrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/Tests/Tests/RestartOrchestrationTests.cs b/test/e2e/Tests/Tests/RestartOrchestrationTests.cs index 5efc359b6..61b26c86f 100644 --- a/test/e2e/Tests/Tests/RestartOrchestrationTests.cs +++ b/test/e2e/Tests/Tests/RestartOrchestrationTests.cs @@ -2,9 +2,9 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System.Net; +using System.Text.Json; using Xunit; using Xunit.Abstractions; -using System.Text.Json; namespace Microsoft.Azure.Durable.Tests.DotnetIsolatedE2E; From 6e1a1fd04ec4ae3807fa48d3135b09e80a9ad368 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Wed, 6 Aug 2025 16:17:29 -0700 Subject: [PATCH 04/15] udpate implementatio --- Directory.Packages.props | 6 ++-- .../TaskHubGrpcServer.cs | 21 +++++++++++ .../DurableTaskClientExtensions.cs | 35 ------------------- .../FunctionsDurableTaskClient.cs | 7 ++++ 4 files changed, 31 insertions(+), 38 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c9571675a..343ca6302 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -28,8 +28,8 @@ - - + + @@ -65,7 +65,7 @@ - + diff --git a/src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs b/src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs index a612ea364..3972ac961 100644 --- a/src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs +++ b/src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs @@ -4,6 +4,7 @@ #nullable enable using System; using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using DurableTask.Core; @@ -401,6 +402,26 @@ private OrchestrationStatus[] GetStatusesNotToOverride() return CreateGetInstanceResponse(state, request); } + public override async Task RestartInstance(P.RestartInstanceRequest request, ServerCallContext context) + { + try + { + string newInstanceId = await this.GetClient(context).RestartAsync(request.InstanceId, request.restartWithNewInstanceId); + return new P.RestartInstanceResponse(newInstanceId); + } + catch (ArgumentException ex) + { + // Thrown when th instanceId is not found. + throw new RpcException(new Status(StatusCode.NotFound, $"ArgumentException: {ex.Message}")); + } + catch (Exception ex) + { + // Any other unexpected exceptions. + throw new RpcException(new Status(StatusCode.Unknown, ex.Message)); + } + + } + #pragma warning disable CS0618 // Type or member is obsolete -- 'internal' usage. private static RawInput Raw(string input) { diff --git a/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs b/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs index fe781879f..fc978ddb2 100644 --- a/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs +++ b/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs @@ -275,41 +275,6 @@ private static ObjectSerializer GetObjectSerializer(HttpResponseData response) ?? throw new InvalidOperationException("A serializer is not configured for the worker."); } - /// - /// Restarts an existing orchestration instance with the original input. - /// - /// The . - /// The ID of the orchestration instance to restart. - /// If true, starts with a new instance ID; otherwise, uses the same instance ID. - /// A token that signals if the operation should be canceled. - /// The new instance ID. - public static async Task RestartAsync( - this DurableTaskClient client, - string instanceId, - bool restartWithNewInstanceId = true, - CancellationToken cancellation = default) - { - // Get the status of the existing instance, including input - OrchestrationMetadata? status = await client.GetInstancesAsync(instanceId, getInputsAndOutputs: true, cancellation) - ?? throw new ArgumentException($"An orchestration with the instanceId {instanceId} was not found."); - - // Deserialize the input. - string orchestratorName = status.Name; - string? input = status.SerializedInput != null - ? System.Text.Json.JsonSerializer.Deserialize(status.SerializedInput) - : null; - - if (restartWithNewInstanceId) - { - return await client.ScheduleNewOrchestrationInstanceAsync(orchestratorName, input, null, cancellation); - } - else - { - var options = new StartOrchestrationOptions { InstanceId = instanceId }; - return await client.ScheduleNewOrchestrationInstanceAsync(orchestratorName, input, options, cancellation); - } - } - private static string? GetBaseUrlFromRequest(HttpRequestData request) { // Default to the scheme from the request URL diff --git a/src/Worker.Extensions.DurableTask/FunctionsDurableTaskClient.cs b/src/Worker.Extensions.DurableTask/FunctionsDurableTaskClient.cs index 3c919362d..b4ee18e6d 100644 --- a/src/Worker.Extensions.DurableTask/FunctionsDurableTaskClient.cs +++ b/src/Worker.Extensions.DurableTask/FunctionsDurableTaskClient.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using System; using System.Threading; using System.Threading.Tasks; using Microsoft.DurableTask; @@ -102,4 +103,10 @@ public override Task WaitForInstanceStartAsync( { return this.inner.WaitForInstanceStartAsync(instanceId, getInputsAndOutputs, cancellation); } + + public override Task RestartAsync( + string instanceId, bool restartWithNewInstanceId = false,CancellationToken cancellation = default) + { + return this.inner.RestartAsync(instanceId, restartWithNewInstanceId, cancellation); + } } From 525462337af27209b2b360f7f3e617662113f7ac Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Thu, 7 Aug 2025 12:35:15 -0700 Subject: [PATCH 05/15] remove unnecessary usings --- src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs | 1 - src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs b/src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs index 3972ac961..2d90573c7 100644 --- a/src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs +++ b/src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs @@ -4,7 +4,6 @@ #nullable enable using System; using System.Diagnostics; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using DurableTask.Core; diff --git a/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs b/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs index fc978ddb2..b1df4ba42 100644 --- a/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs +++ b/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using Azure.Core.Serialization; using Microsoft.Azure.Functions.Worker.Http; -using Microsoft.DurableTask; using Microsoft.DurableTask.Client; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; From 1fab39b6f96d68a7011b1ac513d7e6bd507ca863 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Mon, 18 Aug 2025 15:24:26 -0700 Subject: [PATCH 06/15] don't restart instace with same instanceid if not completed --- .../ContextImplementations/DurableClient.cs | 9 +++++++++ src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableClient.cs b/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableClient.cs index 6f00b6769..1cbedbb30 100644 --- a/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableClient.cs +++ b/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableClient.cs @@ -12,6 +12,7 @@ using DurableTask.Core; using DurableTask.Core.Entities; using DurableTask.Core.History; +using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.WebApiCompatShim; @@ -1158,6 +1159,14 @@ async Task IDurableOrchestrationClient.RestartAsync(string instanceId, b throw new ArgumentException($"An orchestrastion with the instanceId {instanceId} was not found."); } + bool isInstaceNotCompleted = status.RuntimeStatus == OrchestrationRuntimeStatus.Running || status.RuntimeStatus == OrchestrationRuntimeStatus.Pending || status.RuntimeStatus == OrchestrationRuntimeStatus.Suspended; + + if (isInstaceNotCompleted && !restartWithNewInstanceId) + { + throw new InvalidOperationException($"Instance '{instanceId}' cannot be restarted while it is in state '{status.RuntimeStatus}'. " + + "Wait until it has completed, or restart with a new instance ID."); + } + return restartWithNewInstanceId ? await ((IDurableOrchestrationClient)this).StartNewAsync(orchestratorFunctionName: status.Name, status.Input) : await ((IDurableOrchestrationClient)this).StartNewAsync(orchestratorFunctionName: status.Name, instanceId: status.InstanceId, status.Input); } diff --git a/src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs b/src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs index 2d90573c7..1a5dd989b 100644 --- a/src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs +++ b/src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs @@ -413,6 +413,10 @@ private OrchestrationStatus[] GetStatusesNotToOverride() // Thrown when th instanceId is not found. throw new RpcException(new Status(StatusCode.NotFound, $"ArgumentException: {ex.Message}")); } + catch (InvalidOperationException ex) + { + throw new RpcException(new Status(StatusCode.FailedPrecondition, $"InvalidOperationException: {ex.Message}")); + } catch (Exception ex) { // Any other unexpected exceptions. From ea56573b91a09a525337fc11c3cec7c98f943199 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Thu, 11 Sep 2025 11:05:26 -0700 Subject: [PATCH 07/15] update protobuf --- .../Grpc/Protos/orchestrator_service.proto | 43 ++++++++++++++----- .../Grpc/Protos/versions.txt | 4 +- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/src/WebJobs.Extensions.DurableTask/Grpc/Protos/orchestrator_service.proto b/src/WebJobs.Extensions.DurableTask/Grpc/Protos/orchestrator_service.proto index b2ca147b5..3b9c4f408 100644 --- a/src/WebJobs.Extensions.DurableTask/Grpc/Protos/orchestrator_service.proto +++ b/src/WebJobs.Extensions.DurableTask/Grpc/Protos/orchestrator_service.proto @@ -193,7 +193,7 @@ message EntityOperationCalledEvent { } message EntityLockRequestedEvent { - string criticalSectionId = 1; + string criticalSectionId = 1; repeated string lockSet = 2; int32 position = 3; google.protobuf.StringValue parentInstanceId = 4; // used only within messages, null in histories @@ -218,7 +218,7 @@ message EntityUnlockSentEvent { message EntityLockGrantedEvent { string criticalSectionId = 1; } - + message HistoryEvent { int32 eventId = 1; google.protobuf.Timestamp timestamp = 2; @@ -245,8 +245,8 @@ message HistoryEvent { ExecutionResumedEvent executionResumed = 22; EntityOperationSignaledEvent entityOperationSignaled = 23; EntityOperationCalledEvent entityOperationCalled = 24; - EntityOperationCompletedEvent entityOperationCompleted = 25; - EntityOperationFailedEvent entityOperationFailed = 26; + EntityOperationCompletedEvent entityOperationCompleted = 25; + EntityOperationFailedEvent entityOperationFailed = 26; EntityLockRequestedEvent entityLockRequested = 27; EntityLockGrantedEvent entityLockGranted = 28; EntityUnlockSentEvent entityUnlockSent = 29; @@ -258,6 +258,7 @@ message ScheduleTaskAction { google.protobuf.StringValue version = 2; google.protobuf.StringValue input = 3; map tags = 4; + TraceContext parentTraceContext = 5; } message CreateSubOrchestrationAction { @@ -265,6 +266,7 @@ message CreateSubOrchestrationAction { string name = 2; google.protobuf.StringValue version = 3; google.protobuf.StringValue input = 4; + TraceContext parentTraceContext = 5; } message CreateTimerAction { @@ -314,6 +316,11 @@ message OrchestratorAction { } } +message OrchestrationTraceContext { + google.protobuf.StringValue spanID = 1; + google.protobuf.Timestamp spanStartTime = 2; +} + message OrchestratorRequest { string instanceId = 1; google.protobuf.StringValue executionId = 2; @@ -322,6 +329,8 @@ message OrchestratorRequest { OrchestratorEntityParameters entityParameters = 5; bool requiresHistoryStreaming = 6; map properties = 7; + + OrchestrationTraceContext orchestrationTraceContext = 8; } message OrchestratorResponse { @@ -333,6 +342,8 @@ message OrchestratorResponse { // The number of work item events that were processed by the orchestrator. // This field is optional. If not set, the service should assume that the orchestrator processed all events. google.protobuf.Int32Value numEventsProcessed = 5; + + OrchestrationTraceContext orchestrationTraceContext = 6; } message CreateInstanceRequest { @@ -471,6 +482,15 @@ message PurgeInstancesResponse { google.protobuf.BoolValue isComplete = 2; } +message RestartInstanceRequest { + string instanceId = 1; + bool restartWithNewInstanceId = 2; +} + +message RestartInstanceResponse { + string instanceId = 1; +} + message CreateTaskHubRequest { bool recreateIfExists = 1; } @@ -498,7 +518,7 @@ message SignalEntityRequest { } message SignalEntityResponse { - // no payload + // no payload } message GetEntityRequest { @@ -671,18 +691,21 @@ service TaskHubSidecarService { // Rewinds an orchestration instance to last known good state and replays from there. rpc RewindInstance(RewindInstanceRequest) returns (RewindInstanceResponse); + // Restarts an orchestration instance. + rpc RestartInstance(RestartInstanceRequest) returns (RestartInstanceResponse); + // Waits for an orchestration instance to reach a running or completion state. rpc WaitForInstanceStart(GetInstanceRequest) returns (GetInstanceResponse); - + // Waits for an orchestration instance to reach a completion state (completed, failed, terminated, etc.). rpc WaitForInstanceCompletion(GetInstanceRequest) returns (GetInstanceResponse); // Raises an event to a running orchestration instance. rpc RaiseEvent(RaiseEventRequest) returns (RaiseEventResponse); - + // Terminates a running orchestration instance. rpc TerminateInstance(TerminateRequest) returns (TerminateResponse); - + // Suspends a running orchestration instance. rpc SuspendInstance(SuspendRequest) returns (SuspendResponse); @@ -764,7 +787,7 @@ message CompleteTaskResponse { } message HealthPing { - // No payload + // No payload } message StreamInstanceHistoryRequest { @@ -777,4 +800,4 @@ message StreamInstanceHistoryRequest { message HistoryChunk { repeated HistoryEvent events = 1; -} +} \ No newline at end of file diff --git a/src/WebJobs.Extensions.DurableTask/Grpc/Protos/versions.txt b/src/WebJobs.Extensions.DurableTask/Grpc/Protos/versions.txt index c533cb10f..699185838 100644 --- a/src/WebJobs.Extensions.DurableTask/Grpc/Protos/versions.txt +++ b/src/WebJobs.Extensions.DurableTask/Grpc/Protos/versions.txt @@ -1,2 +1,2 @@ -# The following files were downloaded from branch main at 2025-06-12 23:59:39 UTC -https://raw.githubusercontent.com/microsoft/durabletask-protobuf/fd9369c6a03d6af4e95285e432b7c4e943c06970/protos/orchestrator_service.proto +# The following files were downloaded from branch main at 2025-09-11 18:04:38 UTC +https://raw.githubusercontent.com/microsoft/durabletask-protobuf/985035a0890575ae18be0eb2a3ac93c10824498a/protos/orchestrator_service.proto From d379922e8ccf63b173192df978ba05e0fe5c0bed Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Thu, 11 Sep 2025 15:52:25 -0700 Subject: [PATCH 08/15] inc ver and update tests --- Directory.Packages.props | 6 +- .../Grpc/Protos/versions.txt | 2 +- .../TaskHubGrpcServer.cs | 5 +- .../RestartOrchestration.cs | 57 ++++++-- .../IsolatedTestLanguageLocalizer.cs | 4 + .../Tests/Tests/RestartOrchestrationTests.cs | 127 +++++++++++++++++- 6 files changed, 178 insertions(+), 23 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 99a4df039..bad5b6654 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,8 +29,8 @@ - - + + @@ -67,7 +67,7 @@ - + diff --git a/src/WebJobs.Extensions.DurableTask/Grpc/Protos/versions.txt b/src/WebJobs.Extensions.DurableTask/Grpc/Protos/versions.txt index 699185838..1b0140b56 100644 --- a/src/WebJobs.Extensions.DurableTask/Grpc/Protos/versions.txt +++ b/src/WebJobs.Extensions.DurableTask/Grpc/Protos/versions.txt @@ -1,2 +1,2 @@ -# The following files were downloaded from branch main at 2025-09-11 18:04:38 UTC +# The following files were downloaded from branch main at 2025-09-11 18:23:56 UTC https://raw.githubusercontent.com/microsoft/durabletask-protobuf/985035a0890575ae18be0eb2a3ac93c10824498a/protos/orchestrator_service.proto diff --git a/src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs b/src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs index 1a5dd989b..1487de8a8 100644 --- a/src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs +++ b/src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs @@ -405,8 +405,8 @@ private OrchestrationStatus[] GetStatusesNotToOverride() { try { - string newInstanceId = await this.GetClient(context).RestartAsync(request.InstanceId, request.restartWithNewInstanceId); - return new P.RestartInstanceResponse(newInstanceId); + string newInstanceId = await this.GetClient(context).RestartAsync(request.InstanceId, request.RestartWithNewInstanceId); + return new P.RestartInstanceResponse { InstanceId = newInstanceId }; } catch (ArgumentException ex) { @@ -422,7 +422,6 @@ private OrchestrationStatus[] GetStatusesNotToOverride() // Any other unexpected exceptions. throw new RpcException(new Status(StatusCode.Unknown, ex.Message)); } - } #pragma warning disable CS0618 // Type or member is obsolete -- 'internal' usage. diff --git a/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs b/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs index 46c78f7f8..bc1c99b87 100644 --- a/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs +++ b/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. +using Grpc.Core; using Microsoft.Azure.Functions.Worker; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.DurableTask; @@ -12,22 +13,23 @@ namespace Microsoft.Azure.Durable.Tests.E2E; public static class RestartOrchestration { - [Function(nameof(RestartOrchestrator))] - public static string RestartOrchestrator( + [Function(nameof(SimpleOrchestrator))] + public static string SimpleOrchestrator( [OrchestrationTrigger] TaskOrchestrationContext context) { string? input = context.GetInput(); return "Hello " + input; } - [Function("RestartOrchestration_HttpStart")] - public static async Task HttpStartRestartOrchestration( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, - [DurableClient] DurableTaskClient client, - FunctionContext executionContext) + [Function(nameof(WaitForLongOrchestrator))] + public static async Task> WaitForLongOrchestrator( + [OrchestrationTrigger] TaskOrchestrationContext context) { - string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(RestartOrchestrator), input: "World"); - return await client.CreateCheckStatusResponseAsync(req, instanceId); + var outputs = new List(); + + DateTime fireAt = context.CurrentUtcDateTime.AddMinutes(30); + await context.CreateTimer(fireAt: fireAt, cancellationToken: CancellationToken.None); + return outputs; } public class RestartRequest @@ -47,8 +49,43 @@ public static async Task HttpRestartOrchestration( { return req.CreateResponse(HttpStatusCode.BadRequest); } - string newInstanceId = await client.RestartAsync(data.InstanceId,data.RestartWithNewInstanceId); + string newInstanceId = await client.RestartAsync(data.InstanceId, data.RestartWithNewInstanceId); return await client.CreateCheckStatusResponseAsync(req, newInstanceId); } + + [Function("RestartOrchestration_HttpRestartWithErrorHandling")] + public static async Task HttpRestartOrchestrationWithErrorHandling( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, + [DurableClient] DurableTaskClient client, + FunctionContext executionContext) + { + var data = await req.ReadFromJsonAsync(); + if (data == null) + { + return req.CreateResponse(HttpStatusCode.BadRequest); + } + + try + { + string newInstanceId = await client.RestartAsync(data.InstanceId, data.RestartWithNewInstanceId); + var response = req.CreateResponse(HttpStatusCode.OK); + await response.WriteStringAsync(newInstanceId); + return response; + } + catch (RpcException ex) + { + var response = req.CreateResponse(HttpStatusCode.BadRequest); + response.Headers.Add("Content-Type", "application/json"); + + var errorResponse = new + { + StatusCode = ex.StatusCode.ToString(), + Message = ex.Message + }; + + await response.WriteStringAsync(System.Text.Json.JsonSerializer.Serialize(errorResponse)); + return response; + } + } } diff --git a/test/e2e/Tests/Localizers/IsolatedTestLanguageLocalizer.cs b/test/e2e/Tests/Localizers/IsolatedTestLanguageLocalizer.cs index cda3f8142..b1e6ecbe7 100644 --- a/test/e2e/Tests/Localizers/IsolatedTestLanguageLocalizer.cs +++ b/test/e2e/Tests/Localizers/IsolatedTestLanguageLocalizer.cs @@ -22,6 +22,10 @@ internal class IsolatedTestLanguageLocalizer : ITestLanguageLocalizer { "TerminateCompletedInstance.FailureMessage", "InvalidOperationException: Cannot terminate the orchestration instance {0} because instance is in the Completed state." }, { "TerminateTerminatedInstance.FailureMessage", "InvalidOperationException: Cannot terminate the orchestration instance {0} because instance is in the Terminated state." }, { "TerminateInvalidInstance.FailureMessage", "ArgumentException: No instance with ID '{0}' was found." }, + { "RestartInvalidInstance.ErrorName", "NotFound" }, + { "RestartInvalidInstance.ErrorMessage", "ArgumentException: An orchestration with the instanceId '{0}' was not found." }, + { "RestartRunningInstance.ErrorName", "FailedPrecondition" }, + { "RestartRunningInstance.ErrorMessage", "InvalidOperationException: An orchestration with the instanceId {0} cannot be restarted." }, }; public LanguageType GetLanguageType() diff --git a/test/e2e/Tests/Tests/RestartOrchestrationTests.cs b/test/e2e/Tests/Tests/RestartOrchestrationTests.cs index 61b26c86f..ca8ab4e6a 100644 --- a/test/e2e/Tests/Tests/RestartOrchestrationTests.cs +++ b/test/e2e/Tests/Tests/RestartOrchestrationTests.cs @@ -25,14 +25,12 @@ public RestartOrchestrationTests(FunctionAppFixture fixture, ITestOutputHelper t [InlineData(false)] [InlineData(true)] // Test behavior of restartasync of durabletaskclient. - // when restart with a instanceid and startwithnewinstanceid is false, the orchestration should be restarted with the same instance id. - // and the output should be the same as the original orchestration. - // when restart with a instanceid and startwithnewinstanceid is true, the orchestration should be restarted with a new instance id. - // and the output should be same as the original orchestration. + // When restart with a instanceid and startwithnewinstanceid is false, the orchestration should be restarted with the same instance id. + // When restart with a instanceid and startwithnewinstanceid is true, the orchestration should be restarted with a new instance id. public async Task RestartOrchestration_CreatedTimeAndOutputChange(bool restartWithNewInstanceId) { // Start the orchestration - using HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger("RestartOrchestration_HttpStart", ""); + using HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger("StartOrchestration", "?orchestrationName=SimpleOrchestrator"); Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); string statusQueryGetUri = await DurableHelpers.ParseStatusQueryGetUriAsync(response); string instanceId = await DurableHelpers.ParseInstanceIdAsync(response); @@ -64,8 +62,9 @@ public async Task RestartOrchestration_CreatedTimeAndOutputChange(bool restartWi string output2 = restartOrchestrationDetails.Output; DateTime createdTime2 = restartOrchestrationDetails.CreatedTime; - // The outputs and created times should be different + // The outputs should be the same as input is same. Assert.Equal(output1, output2); + // Created time should be different. Assert.NotEqual(createdTime1, createdTime2); if (restartWithNewInstanceId) @@ -78,4 +77,120 @@ public async Task RestartOrchestration_CreatedTimeAndOutputChange(bool restartWi Assert.Equal(instanceId, restartInstanceId); } } + + [Fact] + // Test that if we restart a instanceId that doesn't exist. We will throw NotFound exception. + public async Task RestartOrchestration_NonExistentInstanceId_ShouldReturnNotFound() + { + const string testInstanceId = "nonexistid"; + + // Test restarting with a non-existent instance ID + var restartPayload = new + { + InstanceId = testInstanceId, + RestartWithNewInstanceId = false + }; + + string jsonBody = JsonSerializer.Serialize(restartPayload); + + using HttpResponseMessage restartResponse = await HttpHelpers.InvokeHttpTriggerWithBody( + "RestartOrchestration_HttpRestartWithErrorHandling", jsonBody, "application/json"); + + Assert.Equal(HttpStatusCode.BadRequest, restartResponse.StatusCode); + + string responseContent = await restartResponse.Content.ReadAsStringAsync(); + + // Verify the returned exception contains the correct information. + // In dotnet-isolated, this is the StatusCode of the RPC exception. + Assert.Contains(fixture.functionLanguageLocalizer.GetLocalizedStringValue("RestartInvalidInstance.ErrorName"), responseContent); + + // In dotnet-isolated, this is the deliberate error text from the RpcException + Assert.Contains(fixture.functionLanguageLocalizer.GetLocalizedStringValue("RestartInvalidInstance.ErrorMessage", testInstanceId), responseContent); + } + + [Fact] + // Test that if we restart a instance that doesn't reach to completed state, + // If RestartWithNewInstanceId is set to false, a PreconditionFailed error will be thrown. + public async Task RestartOrchestration_RunningOrchestrationWithRestartFalse_ShouldReturnFailedPrecondition() + { + // Start a long-running orchestration + using HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger("StartOrchestration", "?orchestrationName=WaitForLongOrchestrator"); + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + string instanceId = await DurableHelpers.ParseInstanceIdAsync(response); + string statusQueryGetUri = await DurableHelpers.ParseStatusQueryGetUriAsync(response); + + // Wait for the orchestration to be running + await DurableHelpers.WaitForOrchestrationStateAsync(statusQueryGetUri, "Running", 30); + + // Try to restart the running orchestration with restartWithNewInstanceId = false + var restartPayload = new + { + InstanceId = instanceId, + RestartWithNewInstanceId = false + }; + + string jsonBody = JsonSerializer.Serialize(restartPayload); + + using HttpResponseMessage restartResponse = await HttpHelpers.InvokeHttpTriggerWithBody( + "RestartOrchestration_HttpRestartWithErrorHandling", jsonBody, "application/json"); + + Assert.Equal(HttpStatusCode.BadRequest, restartResponse.StatusCode); + + string responseContent = await restartResponse.Content.ReadAsStringAsync(); + + // Verify the returned exception contains the correct information. + Assert.Contains(fixture.functionLanguageLocalizer.GetLocalizedStringValue("RestartRunningInstance.ErrorName"), responseContent); + Assert.Contains(fixture.functionLanguageLocalizer.GetLocalizedStringValue("RestartRunningInstance.ErrorMessage", instanceId), responseContent); + + // Clean up: terminate the long-running orchestration + using HttpResponseMessage terminateResponse = await HttpHelpers.InvokeHttpTrigger("TerminateInstance", $"?instanceId={instanceId}"); + Assert.Equal(HttpStatusCode.OK, terminateResponse.StatusCode); + } + + [Fact] + // Test that if we restart a instance that doesn't reach to completed state, + // If RestartWithNewInstanceId is set to true, a new instanceId will be returned for this orchestrator. + public async Task RestartOrchestration_RunningOrchestrationWithRestartTrue_ShouldReturnNewInstanceId() + { + // Start a long-running orchestration + using HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger("StartOrchestration", "?orchestrationName=WaitForLongOrchestrator"); + Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); + string originalInstanceId = await DurableHelpers.ParseInstanceIdAsync(response); + string statusQueryGetUri = await DurableHelpers.ParseStatusQueryGetUriAsync(response); + + // Wait for the orchestration to be running + await DurableHelpers.WaitForOrchestrationStateAsync(statusQueryGetUri, "Running", 30); + + // Try to restart the running orchestration with restartWithNewInstanceId = true + var restartPayload = new + { + InstanceId = originalInstanceId, + RestartWithNewInstanceId = true + }; + + string jsonBody = JsonSerializer.Serialize(restartPayload); + + using HttpResponseMessage restartResponse = await HttpHelpers.InvokeHttpTriggerWithBody( + "RestartOrchestration_HttpRestartWithErrorHandling", jsonBody, "application/json"); + + Assert.Equal(HttpStatusCode.OK, restartResponse.StatusCode); + + string responseContent = await restartResponse.Content.ReadAsStringAsync(); + string newInstanceId = responseContent.Trim('"'); + + // The new instance ID should be different from the original + Assert.NotEqual(originalInstanceId, newInstanceId); + Assert.NotEmpty(newInstanceId); + + // Verify the new orchestration is running + string newStatusQueryGetUri = await DurableHelpers.ParseStatusQueryGetUriAsync(restartResponse); + await DurableHelpers.WaitForOrchestrationStateAsync(newStatusQueryGetUri, "Running", 30); + + // Clean up: terminate both orchestrations + using HttpResponseMessage terminateOriginalResponse = await HttpHelpers.InvokeHttpTrigger("TerminateInstance", $"?instanceId={originalInstanceId}"); + Assert.Equal(HttpStatusCode.OK, terminateOriginalResponse.StatusCode); + + using HttpResponseMessage terminateNewResponse = await HttpHelpers.InvokeHttpTrigger("TerminateInstance", $"?instanceId={newInstanceId}"); + Assert.Equal(HttpStatusCode.OK, terminateNewResponse.StatusCode); + } } From 4c2fdbb3c7352484cb764ea6f0e8afaf6b4947e5 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Fri, 12 Sep 2025 11:19:52 -0700 Subject: [PATCH 09/15] inc ver and update test --- .../WebJobs.Extensions.DurableTask.csproj | 4 +- .../AssemblyInfo.cs | 2 +- .../Worker.Extensions.DurableTask.csproj | 3 +- .../RestartOrchestration.cs | 12 ++-- test/e2e/Apps/BasicNode/extensions.csproj | 2 +- .../Apps/BasicPowerShell/extensions.csproj | 2 +- test/e2e/Apps/BasicPython/extensions.csproj | 2 +- .../IsolatedTestLanguageLocalizer.cs | 6 +- .../Tests/Tests/RestartOrchestrationTests.cs | 62 ++----------------- 9 files changed, 17 insertions(+), 78 deletions(-) diff --git a/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj b/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj index 3c68f3b91..3499faa64 100644 --- a/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj +++ b/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj @@ -5,8 +5,8 @@ Microsoft.Azure.WebJobs.Extensions.DurableTask Microsoft.Azure.WebJobs.Extensions.DurableTask 3 - 3 - 1 + 5 + 0 $(MajorVersion).$(MinorVersion).$(PatchVersion) $(MajorVersion).$(MinorVersion).$(PatchVersion) $(MajorVersion).0.0.0 diff --git a/src/Worker.Extensions.DurableTask/AssemblyInfo.cs b/src/Worker.Extensions.DurableTask/AssemblyInfo.cs index 5a4814315..061ba9228 100644 --- a/src/Worker.Extensions.DurableTask/AssemblyInfo.cs +++ b/src/Worker.Extensions.DurableTask/AssemblyInfo.cs @@ -5,5 +5,5 @@ using Microsoft.Azure.Functions.Worker.Extensions.Abstractions; // TODO: Find a way to generate this dynamically at build-time -[assembly: ExtensionInformation("Microsoft.Azure.WebJobs.Extensions.DurableTask", "3.3.1")] +[assembly: ExtensionInformation("Microsoft.Azure.WebJobs.Extensions.DurableTask", "3.5.0")] [assembly: InternalsVisibleTo("Worker.Extensions.DurableTask.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100cd1dabd5a893b40e75dc901fe7293db4a3caf9cd4d3e3ed6178d49cd476969abe74a9e0b7f4a0bb15edca48758155d35a4f05e6e852fff1b319d103b39ba04acbadd278c2753627c95e1f6f6582425374b92f51cca3deb0d2aab9de3ecda7753900a31f70a236f163006beefffe282888f85e3c76d1205ec7dfef7fa472a17b1")] diff --git a/src/Worker.Extensions.DurableTask/Worker.Extensions.DurableTask.csproj b/src/Worker.Extensions.DurableTask/Worker.Extensions.DurableTask.csproj index e0053b816..52ddabb3c 100644 --- a/src/Worker.Extensions.DurableTask/Worker.Extensions.DurableTask.csproj +++ b/src/Worker.Extensions.DurableTask/Worker.Extensions.DurableTask.csproj @@ -29,8 +29,7 @@ ..\..\sign.snk - 1.6.1 - + 1.8.0 $(VersionPrefix).0 $(VersionPrefix).$(FileVersionRevision) diff --git a/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs b/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs index bc1c99b87..d40e8eb42 100644 --- a/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs +++ b/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs @@ -73,18 +73,14 @@ public static async Task HttpRestartOrchestrationWithErrorHand await response.WriteStringAsync(newInstanceId); return response; } - catch (RpcException ex) + catch (Exception ex) { var response = req.CreateResponse(HttpStatusCode.BadRequest); response.Headers.Add("Content-Type", "application/json"); + + string message = ex.Message; - var errorResponse = new - { - StatusCode = ex.StatusCode.ToString(), - Message = ex.Message - }; - - await response.WriteStringAsync(System.Text.Json.JsonSerializer.Serialize(errorResponse)); + await response.WriteStringAsync(message); return response; } } diff --git a/test/e2e/Apps/BasicNode/extensions.csproj b/test/e2e/Apps/BasicNode/extensions.csproj index c95e33036..d7033049d 100644 --- a/test/e2e/Apps/BasicNode/extensions.csproj +++ b/test/e2e/Apps/BasicNode/extensions.csproj @@ -11,7 +11,7 @@ - + diff --git a/test/e2e/Apps/BasicPowerShell/extensions.csproj b/test/e2e/Apps/BasicPowerShell/extensions.csproj index c95e33036..d7033049d 100644 --- a/test/e2e/Apps/BasicPowerShell/extensions.csproj +++ b/test/e2e/Apps/BasicPowerShell/extensions.csproj @@ -11,7 +11,7 @@ - + diff --git a/test/e2e/Apps/BasicPython/extensions.csproj b/test/e2e/Apps/BasicPython/extensions.csproj index c95e33036..d7033049d 100644 --- a/test/e2e/Apps/BasicPython/extensions.csproj +++ b/test/e2e/Apps/BasicPython/extensions.csproj @@ -11,7 +11,7 @@ - + diff --git a/test/e2e/Tests/Localizers/IsolatedTestLanguageLocalizer.cs b/test/e2e/Tests/Localizers/IsolatedTestLanguageLocalizer.cs index b1e6ecbe7..73157a8f5 100644 --- a/test/e2e/Tests/Localizers/IsolatedTestLanguageLocalizer.cs +++ b/test/e2e/Tests/Localizers/IsolatedTestLanguageLocalizer.cs @@ -22,10 +22,8 @@ internal class IsolatedTestLanguageLocalizer : ITestLanguageLocalizer { "TerminateCompletedInstance.FailureMessage", "InvalidOperationException: Cannot terminate the orchestration instance {0} because instance is in the Completed state." }, { "TerminateTerminatedInstance.FailureMessage", "InvalidOperationException: Cannot terminate the orchestration instance {0} because instance is in the Terminated state." }, { "TerminateInvalidInstance.FailureMessage", "ArgumentException: No instance with ID '{0}' was found." }, - { "RestartInvalidInstance.ErrorName", "NotFound" }, - { "RestartInvalidInstance.ErrorMessage", "ArgumentException: An orchestration with the instanceId '{0}' was not found." }, - { "RestartRunningInstance.ErrorName", "FailedPrecondition" }, - { "RestartRunningInstance.ErrorMessage", "InvalidOperationException: An orchestration with the instanceId {0} cannot be restarted." }, + { "RestartInvalidInstance.ErrorMessage", "An orchestration with the instanceId {0} was not found." }, + { "RestartRunningInstance.ErrorMessage", "An orchestration with the instanceId {0} cannot be restarted." }, }; public LanguageType GetLanguageType() diff --git a/test/e2e/Tests/Tests/RestartOrchestrationTests.cs b/test/e2e/Tests/Tests/RestartOrchestrationTests.cs index ca8ab4e6a..71636c351 100644 --- a/test/e2e/Tests/Tests/RestartOrchestrationTests.cs +++ b/test/e2e/Tests/Tests/RestartOrchestrationTests.cs @@ -79,7 +79,7 @@ public async Task RestartOrchestration_CreatedTimeAndOutputChange(bool restartWi } [Fact] - // Test that if we restart a instanceId that doesn't exist. We will throw NotFound exception. + // Test that if we restart a instanceId that doesn't exist. We will throw ArgumentException exception. public async Task RestartOrchestration_NonExistentInstanceId_ShouldReturnNotFound() { const string testInstanceId = "nonexistid"; @@ -95,23 +95,17 @@ public async Task RestartOrchestration_NonExistentInstanceId_ShouldReturnNotFoun using HttpResponseMessage restartResponse = await HttpHelpers.InvokeHttpTriggerWithBody( "RestartOrchestration_HttpRestartWithErrorHandling", jsonBody, "application/json"); - - Assert.Equal(HttpStatusCode.BadRequest, restartResponse.StatusCode); string responseContent = await restartResponse.Content.ReadAsStringAsync(); - - // Verify the returned exception contains the correct information. - // In dotnet-isolated, this is the StatusCode of the RPC exception. - Assert.Contains(fixture.functionLanguageLocalizer.GetLocalizedStringValue("RestartInvalidInstance.ErrorName"), responseContent); - // In dotnet-isolated, this is the deliberate error text from the RpcException + // Verfity we weill return the right exception message. Assert.Contains(fixture.functionLanguageLocalizer.GetLocalizedStringValue("RestartInvalidInstance.ErrorMessage", testInstanceId), responseContent); } [Fact] // Test that if we restart a instance that doesn't reach to completed state, - // If RestartWithNewInstanceId is set to false, a PreconditionFailed error will be thrown. - public async Task RestartOrchestration_RunningOrchestrationWithRestartFalse_ShouldReturnFailedPrecondition() + // If RestartWithNewInstanceId is set to false, a InvalidOperationException error will be thrown. + public async Task RestartOrchestration_NotCompletedOrchestrationWithRestartFalse_ShouldReturnFailedPrecondition() { // Start a long-running orchestration using HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger("StartOrchestration", "?orchestrationName=WaitForLongOrchestrator"); @@ -139,58 +133,10 @@ public async Task RestartOrchestration_RunningOrchestrationWithRestartFalse_Shou string responseContent = await restartResponse.Content.ReadAsStringAsync(); // Verify the returned exception contains the correct information. - Assert.Contains(fixture.functionLanguageLocalizer.GetLocalizedStringValue("RestartRunningInstance.ErrorName"), responseContent); Assert.Contains(fixture.functionLanguageLocalizer.GetLocalizedStringValue("RestartRunningInstance.ErrorMessage", instanceId), responseContent); // Clean up: terminate the long-running orchestration using HttpResponseMessage terminateResponse = await HttpHelpers.InvokeHttpTrigger("TerminateInstance", $"?instanceId={instanceId}"); Assert.Equal(HttpStatusCode.OK, terminateResponse.StatusCode); } - - [Fact] - // Test that if we restart a instance that doesn't reach to completed state, - // If RestartWithNewInstanceId is set to true, a new instanceId will be returned for this orchestrator. - public async Task RestartOrchestration_RunningOrchestrationWithRestartTrue_ShouldReturnNewInstanceId() - { - // Start a long-running orchestration - using HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger("StartOrchestration", "?orchestrationName=WaitForLongOrchestrator"); - Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); - string originalInstanceId = await DurableHelpers.ParseInstanceIdAsync(response); - string statusQueryGetUri = await DurableHelpers.ParseStatusQueryGetUriAsync(response); - - // Wait for the orchestration to be running - await DurableHelpers.WaitForOrchestrationStateAsync(statusQueryGetUri, "Running", 30); - - // Try to restart the running orchestration with restartWithNewInstanceId = true - var restartPayload = new - { - InstanceId = originalInstanceId, - RestartWithNewInstanceId = true - }; - - string jsonBody = JsonSerializer.Serialize(restartPayload); - - using HttpResponseMessage restartResponse = await HttpHelpers.InvokeHttpTriggerWithBody( - "RestartOrchestration_HttpRestartWithErrorHandling", jsonBody, "application/json"); - - Assert.Equal(HttpStatusCode.OK, restartResponse.StatusCode); - - string responseContent = await restartResponse.Content.ReadAsStringAsync(); - string newInstanceId = responseContent.Trim('"'); - - // The new instance ID should be different from the original - Assert.NotEqual(originalInstanceId, newInstanceId); - Assert.NotEmpty(newInstanceId); - - // Verify the new orchestration is running - string newStatusQueryGetUri = await DurableHelpers.ParseStatusQueryGetUriAsync(restartResponse); - await DurableHelpers.WaitForOrchestrationStateAsync(newStatusQueryGetUri, "Running", 30); - - // Clean up: terminate both orchestrations - using HttpResponseMessage terminateOriginalResponse = await HttpHelpers.InvokeHttpTrigger("TerminateInstance", $"?instanceId={originalInstanceId}"); - Assert.Equal(HttpStatusCode.OK, terminateOriginalResponse.StatusCode); - - using HttpResponseMessage terminateNewResponse = await HttpHelpers.InvokeHttpTrigger("TerminateInstance", $"?instanceId={newInstanceId}"); - Assert.Equal(HttpStatusCode.OK, terminateNewResponse.StatusCode); - } } From dcddb6e3eef5db5cf301586e412a49e4202875fb Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Fri, 12 Sep 2025 11:26:52 -0700 Subject: [PATCH 10/15] undo unnecessary change --- .../Worker.Extensions.DurableTask.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Worker.Extensions.DurableTask/Worker.Extensions.DurableTask.csproj b/src/Worker.Extensions.DurableTask/Worker.Extensions.DurableTask.csproj index 52ddabb3c..afb5ab724 100644 --- a/src/Worker.Extensions.DurableTask/Worker.Extensions.DurableTask.csproj +++ b/src/Worker.Extensions.DurableTask/Worker.Extensions.DurableTask.csproj @@ -30,6 +30,7 @@ 1.8.0 + $(VersionPrefix).0 $(VersionPrefix).$(FileVersionRevision) From e91868d189b15c2c9f36b0310504eee3d444255c Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Fri, 12 Sep 2025 13:11:46 -0700 Subject: [PATCH 11/15] skip non dotnet language for test --- .../BackendSmokeTests/MSSQL/app.config | 11 ++++++++++ .../DotNetIsolated/nuget.config | 20 +++++++++++++++++++ .../durableJava/build/tmp/jar/MANIFEST.MF | 2 ++ .../Tests/Tests/RestartOrchestrationTests.cs | 9 +++++++++ 4 files changed, 42 insertions(+) create mode 100644 test/SmokeTests/BackendSmokeTests/MSSQL/app.config create mode 100644 test/SmokeTests/OOProcSmokeTests/DotNetIsolated/nuget.config create mode 100644 test/SmokeTests/OOProcSmokeTests/durableJava/build/tmp/jar/MANIFEST.MF diff --git a/test/SmokeTests/BackendSmokeTests/MSSQL/app.config b/test/SmokeTests/BackendSmokeTests/MSSQL/app.config new file mode 100644 index 000000000..454976034 --- /dev/null +++ b/test/SmokeTests/BackendSmokeTests/MSSQL/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/test/SmokeTests/OOProcSmokeTests/DotNetIsolated/nuget.config b/test/SmokeTests/OOProcSmokeTests/DotNetIsolated/nuget.config new file mode 100644 index 000000000..663255354 --- /dev/null +++ b/test/SmokeTests/OOProcSmokeTests/DotNetIsolated/nuget.config @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/SmokeTests/OOProcSmokeTests/durableJava/build/tmp/jar/MANIFEST.MF b/test/SmokeTests/OOProcSmokeTests/durableJava/build/tmp/jar/MANIFEST.MF new file mode 100644 index 000000000..59499bce4 --- /dev/null +++ b/test/SmokeTests/OOProcSmokeTests/durableJava/build/tmp/jar/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 + diff --git a/test/e2e/Tests/Tests/RestartOrchestrationTests.cs b/test/e2e/Tests/Tests/RestartOrchestrationTests.cs index 71636c351..b36829f4f 100644 --- a/test/e2e/Tests/Tests/RestartOrchestrationTests.cs +++ b/test/e2e/Tests/Tests/RestartOrchestrationTests.cs @@ -24,6 +24,9 @@ public RestartOrchestrationTests(FunctionAppFixture fixture, ITestOutputHelper t [Theory] [InlineData(false)] [InlineData(true)] + [Trait("PowerShell", "Skip")] // RestartAsync not yet implemented in PowerShell + [Trait("Java", "Skip")] // RestartAsync not yet implemented in Java + [Trait("Python", "Skip")] // RestartAsync not supported in Python // Test behavior of restartasync of durabletaskclient. // When restart with a instanceid and startwithnewinstanceid is false, the orchestration should be restarted with the same instance id. // When restart with a instanceid and startwithnewinstanceid is true, the orchestration should be restarted with a new instance id. @@ -79,6 +82,9 @@ public async Task RestartOrchestration_CreatedTimeAndOutputChange(bool restartWi } [Fact] + [Trait("PowerShell", "Skip")] // RestartAsync not yet implemented in PowerShell + [Trait("Java", "Skip")] // RestartAsync not yet implemented in Java + [Trait("Python", "Skip")] // RestartAsync not supported in Python // Test that if we restart a instanceId that doesn't exist. We will throw ArgumentException exception. public async Task RestartOrchestration_NonExistentInstanceId_ShouldReturnNotFound() { @@ -103,6 +109,9 @@ public async Task RestartOrchestration_NonExistentInstanceId_ShouldReturnNotFoun } [Fact] + [Trait("PowerShell", "Skip")] // RestartAsync not yet implemented in PowerShell + [Trait("Java", "Skip")] // RestartAsync not yet implemented in Java + [Trait("Python", "Skip")] // RestartAsync not supported in Python // Test that if we restart a instance that doesn't reach to completed state, // If RestartWithNewInstanceId is set to false, a InvalidOperationException error will be thrown. public async Task RestartOrchestration_NotCompletedOrchestrationWithRestartFalse_ShouldReturnFailedPrecondition() From 81996775398e2881dbdea3ead6eeb2ad6d42ef3e Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Fri, 12 Sep 2025 13:51:44 -0700 Subject: [PATCH 12/15] updates test --- .../RestartOrchestration.cs | 24 +++++++++++++++++-- test/e2e/Apps/BasicJava/extensions.csproj | 2 +- .../Tests/Tests/RestartOrchestrationTests.cs | 7 ++++-- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs b/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs index d40e8eb42..192b1e728 100644 --- a/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs +++ b/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs @@ -21,8 +21,8 @@ public static string SimpleOrchestrator( return "Hello " + input; } - [Function(nameof(WaitForLongOrchestrator))] - public static async Task> WaitForLongOrchestrator( + [Function(nameof(LongOrchestrator))] + public static async Task> LongOrchestrator( [OrchestrationTrigger] TaskOrchestrationContext context) { var outputs = new List(); @@ -38,6 +38,26 @@ public class RestartRequest public bool RestartWithNewInstanceId { get; set; } } + [Function("RestartOrchestration_HttpStart")] + public static async Task HttpStart( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "RestarttOrchestration_HttpStart/{orchestratorName}")] HttpRequestData req, + string orchestratorName, + [DurableClient] DurableTaskClient client, + FunctionContext executionContext) + { + ILogger logger = executionContext.GetLogger("Function1_HttpStart"); + + // Function input comes from the request content. + string instanceId = await client.ScheduleNewOrchestrationInstanceAsync( + orchestratorName); + + logger.LogInformation("Started orchestration with ID = '{instanceId}'.", instanceId); + + // Returns an HTTP 202 response with an instance management payload. + // See https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-http-api#start-orchestration + return await client.CreateCheckStatusResponseAsync(req, instanceId); + } + [Function("RestartOrchestration_HttpRestart")] public static async Task HttpRestartOrchestration( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, diff --git a/test/e2e/Apps/BasicJava/extensions.csproj b/test/e2e/Apps/BasicJava/extensions.csproj index 11cdc507f..7985c564a 100644 --- a/test/e2e/Apps/BasicJava/extensions.csproj +++ b/test/e2e/Apps/BasicJava/extensions.csproj @@ -11,7 +11,7 @@ - + diff --git a/test/e2e/Tests/Tests/RestartOrchestrationTests.cs b/test/e2e/Tests/Tests/RestartOrchestrationTests.cs index b36829f4f..fe627a7c2 100644 --- a/test/e2e/Tests/Tests/RestartOrchestrationTests.cs +++ b/test/e2e/Tests/Tests/RestartOrchestrationTests.cs @@ -27,13 +27,14 @@ public RestartOrchestrationTests(FunctionAppFixture fixture, ITestOutputHelper t [Trait("PowerShell", "Skip")] // RestartAsync not yet implemented in PowerShell [Trait("Java", "Skip")] // RestartAsync not yet implemented in Java [Trait("Python", "Skip")] // RestartAsync not supported in Python + [Trait("Node", "Skip")] // RestartAsync not supported in Node // Test behavior of restartasync of durabletaskclient. // When restart with a instanceid and startwithnewinstanceid is false, the orchestration should be restarted with the same instance id. // When restart with a instanceid and startwithnewinstanceid is true, the orchestration should be restarted with a new instance id. public async Task RestartOrchestration_CreatedTimeAndOutputChange(bool restartWithNewInstanceId) { // Start the orchestration - using HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger("StartOrchestration", "?orchestrationName=SimpleOrchestrator"); + using HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger("RestarttOrchestration_HttpStart/SimpleOrchestrator"); Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); string statusQueryGetUri = await DurableHelpers.ParseStatusQueryGetUriAsync(response); string instanceId = await DurableHelpers.ParseInstanceIdAsync(response); @@ -85,6 +86,7 @@ public async Task RestartOrchestration_CreatedTimeAndOutputChange(bool restartWi [Trait("PowerShell", "Skip")] // RestartAsync not yet implemented in PowerShell [Trait("Java", "Skip")] // RestartAsync not yet implemented in Java [Trait("Python", "Skip")] // RestartAsync not supported in Python + [Trait("Node", "Skip")] // RestartAsync not supported in Node // Test that if we restart a instanceId that doesn't exist. We will throw ArgumentException exception. public async Task RestartOrchestration_NonExistentInstanceId_ShouldReturnNotFound() { @@ -112,12 +114,13 @@ public async Task RestartOrchestration_NonExistentInstanceId_ShouldReturnNotFoun [Trait("PowerShell", "Skip")] // RestartAsync not yet implemented in PowerShell [Trait("Java", "Skip")] // RestartAsync not yet implemented in Java [Trait("Python", "Skip")] // RestartAsync not supported in Python + [Trait("Node", "Skip")] // RestartAsync not supported in Node // Test that if we restart a instance that doesn't reach to completed state, // If RestartWithNewInstanceId is set to false, a InvalidOperationException error will be thrown. public async Task RestartOrchestration_NotCompletedOrchestrationWithRestartFalse_ShouldReturnFailedPrecondition() { // Start a long-running orchestration - using HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger("StartOrchestration", "?orchestrationName=WaitForLongOrchestrator"); + using HttpResponseMessage response = await HttpHelpers.InvokeHttpTrigger("RestarttOrchestration_HttpStart/LongOrchestrator"); Assert.Equal(HttpStatusCode.Accepted, response.StatusCode); string instanceId = await DurableHelpers.ParseInstanceIdAsync(response); string statusQueryGetUri = await DurableHelpers.ParseStatusQueryGetUriAsync(response); From d3888deab9a1ce936c71743f84be981afb340e11 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Mon, 15 Sep 2025 10:13:30 -0700 Subject: [PATCH 13/15] remove unnecessary local added doc --- .../BackendSmokeTests/MSSQL/app.config | 11 ---------- .../DotNetIsolated/nuget.config | 20 ------------------- .../durableJava/build/tmp/jar/MANIFEST.MF | 2 -- .../RestartOrchestration.cs | 10 ++++++---- 4 files changed, 6 insertions(+), 37 deletions(-) delete mode 100644 test/SmokeTests/BackendSmokeTests/MSSQL/app.config delete mode 100644 test/SmokeTests/OOProcSmokeTests/DotNetIsolated/nuget.config delete mode 100644 test/SmokeTests/OOProcSmokeTests/durableJava/build/tmp/jar/MANIFEST.MF diff --git a/test/SmokeTests/BackendSmokeTests/MSSQL/app.config b/test/SmokeTests/BackendSmokeTests/MSSQL/app.config deleted file mode 100644 index 454976034..000000000 --- a/test/SmokeTests/BackendSmokeTests/MSSQL/app.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/test/SmokeTests/OOProcSmokeTests/DotNetIsolated/nuget.config b/test/SmokeTests/OOProcSmokeTests/DotNetIsolated/nuget.config deleted file mode 100644 index 663255354..000000000 --- a/test/SmokeTests/OOProcSmokeTests/DotNetIsolated/nuget.config +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/test/SmokeTests/OOProcSmokeTests/durableJava/build/tmp/jar/MANIFEST.MF b/test/SmokeTests/OOProcSmokeTests/durableJava/build/tmp/jar/MANIFEST.MF deleted file mode 100644 index 59499bce4..000000000 --- a/test/SmokeTests/OOProcSmokeTests/durableJava/build/tmp/jar/MANIFEST.MF +++ /dev/null @@ -1,2 +0,0 @@ -Manifest-Version: 1.0 - diff --git a/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs b/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs index 192b1e728..583f70146 100644 --- a/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs +++ b/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs @@ -21,6 +21,8 @@ public static string SimpleOrchestrator( return "Hello " + input; } + // Orchestration that waits on a long-running timer. + // Used for testing restart of an orchestration that has not yet completed. [Function(nameof(LongOrchestrator))] public static async Task> LongOrchestrator( [OrchestrationTrigger] TaskOrchestrationContext context) @@ -38,6 +40,7 @@ public class RestartRequest public bool RestartWithNewInstanceId { get; set; } } + // HTTP-triggered function that starts a new durable orchestration instance. [Function("RestartOrchestration_HttpStart")] public static async Task HttpStart( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "RestarttOrchestration_HttpStart/{orchestratorName}")] HttpRequestData req, @@ -47,17 +50,14 @@ public static async Task HttpStart( { ILogger logger = executionContext.GetLogger("Function1_HttpStart"); - // Function input comes from the request content. string instanceId = await client.ScheduleNewOrchestrationInstanceAsync( orchestratorName); logger.LogInformation("Started orchestration with ID = '{instanceId}'.", instanceId); - - // Returns an HTTP 202 response with an instance management payload. - // See https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-http-api#start-orchestration return await client.CreateCheckStatusResponseAsync(req, instanceId); } + // HTTP-triggered function that restarts a durable orchestration instance using the provided instance ID and restart options. [Function("RestartOrchestration_HttpRestart")] public static async Task HttpRestartOrchestration( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, @@ -74,6 +74,8 @@ public static async Task HttpRestartOrchestration( return await client.CreateCheckStatusResponseAsync(req, newInstanceId); } + // HTTP-triggered function that restarts a durable orchestration instance with comprehensive error handling. + // Returns the new instance ID on success, or returns the error message with appropriate HTTP status codes on failure. [Function("RestartOrchestration_HttpRestartWithErrorHandling")] public static async Task HttpRestartOrchestrationWithErrorHandling( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, From 3c35c9afe08cce602d2ca674958da8205dd8f7df Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Mon, 15 Sep 2025 11:46:18 -0700 Subject: [PATCH 14/15] update bycomments --- .../ContextImplementations/DurableClient.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableClient.cs b/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableClient.cs index 1cbedbb30..b1d267c08 100644 --- a/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableClient.cs +++ b/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableClient.cs @@ -1159,7 +1159,9 @@ async Task IDurableOrchestrationClient.RestartAsync(string instanceId, b throw new ArgumentException($"An orchestrastion with the instanceId {instanceId} was not found."); } - bool isInstaceNotCompleted = status.RuntimeStatus == OrchestrationRuntimeStatus.Running || status.RuntimeStatus == OrchestrationRuntimeStatus.Pending || status.RuntimeStatus == OrchestrationRuntimeStatus.Suspended; + bool isInstaceNotCompleted = status.RuntimeStatus == OrchestrationRuntimeStatus.Running || + status.RuntimeStatus == OrchestrationRuntimeStatus.Pending || + status.RuntimeStatus == OrchestrationRuntimeStatus.Suspended; if (isInstaceNotCompleted && !restartWithNewInstanceId) { From 43c83aa90ba57a4e03964a836d77e9923e6646da Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Mon, 15 Sep 2025 11:47:10 -0700 Subject: [PATCH 15/15] remove using --- .../ContextImplementations/DurableClient.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableClient.cs b/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableClient.cs index b1d267c08..a2fcc298c 100644 --- a/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableClient.cs +++ b/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableClient.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Net.Http; using System.Threading; @@ -12,7 +11,6 @@ using DurableTask.Core; using DurableTask.Core.Entities; using DurableTask.Core.History; -using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.WebApiCompatShim;