diff --git a/Directory.Packages.props b/Directory.Packages.props index affb6c970..e5ba73485 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,8 +29,8 @@ - - + + @@ -67,7 +67,7 @@ - + diff --git a/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableClient.cs b/src/WebJobs.Extensions.DurableTask/ContextImplementations/DurableClient.cs index 6f00b6769..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; @@ -1158,6 +1157,16 @@ 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/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..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-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: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 a612ea364..1487de8a8 100644 --- a/src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs +++ b/src/WebJobs.Extensions.DurableTask/TaskHubGrpcServer.cs @@ -401,6 +401,29 @@ 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 { InstanceId = newInstanceId }; + } + catch (ArgumentException ex) + { + // 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. + 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/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj b/src/WebJobs.Extensions.DurableTask/WebJobs.Extensions.DurableTask.csproj index ff4db65f9..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 - 4 - 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 88e2f21cd..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.4.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/DurableTaskClientExtensions.cs b/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs index 044d2ee10..545260ac6 100644 --- a/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs +++ b/src/Worker.Extensions.DurableTask/DurableTaskClientExtensions.cs @@ -259,6 +259,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), @@ -319,7 +320,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/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); + } } diff --git a/src/Worker.Extensions.DurableTask/Worker.Extensions.DurableTask.csproj b/src/Worker.Extensions.DurableTask/Worker.Extensions.DurableTask.csproj index 475f896a3..afb5ab724 100644 --- a/src/Worker.Extensions.DurableTask/Worker.Extensions.DurableTask.csproj +++ b/src/Worker.Extensions.DurableTask/Worker.Extensions.DurableTask.csproj @@ -29,7 +29,7 @@ ..\..\sign.snk - 1.7.1 + 1.8.0 $(VersionPrefix).0 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..583f70146 --- /dev/null +++ b/test/e2e/Apps/BasicDotNetIsolated/RestartOrchestration.cs @@ -0,0 +1,109 @@ +// 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; +using Microsoft.DurableTask.Client; +using Microsoft.Extensions.Logging; +using System.Net; + +namespace Microsoft.Azure.Durable.Tests.E2E; + +public static class RestartOrchestration +{ + [Function(nameof(SimpleOrchestrator))] + public static string SimpleOrchestrator( + [OrchestrationTrigger] TaskOrchestrationContext context) + { + string? input = context.GetInput(); + 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) + { + var outputs = new List(); + + DateTime fireAt = context.CurrentUtcDateTime.AddMinutes(30); + await context.CreateTimer(fireAt: fireAt, cancellationToken: CancellationToken.None); + return outputs; + } + + public class RestartRequest + { + public string InstanceId { get; set; } = string.Empty; + 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, + string orchestratorName, + [DurableClient] DurableTaskClient client, + FunctionContext executionContext) + { + ILogger logger = executionContext.GetLogger("Function1_HttpStart"); + + string instanceId = await client.ScheduleNewOrchestrationInstanceAsync( + orchestratorName); + + logger.LogInformation("Started orchestration with ID = '{instanceId}'.", instanceId); + 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, + [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); + } + + // 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, + [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 (Exception ex) + { + var response = req.CreateResponse(HttpStatusCode.BadRequest); + response.Headers.Add("Content-Type", "application/json"); + + string message = ex.Message; + + await response.WriteStringAsync(message); + return response; + } + } +} 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/Apps/BasicNode/extensions.csproj b/test/e2e/Apps/BasicNode/extensions.csproj index cbede337b..7985c564a 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 cbede337b..7985c564a 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 cbede337b..7985c564a 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 cda3f8142..73157a8f5 100644 --- a/test/e2e/Tests/Localizers/IsolatedTestLanguageLocalizer.cs +++ b/test/e2e/Tests/Localizers/IsolatedTestLanguageLocalizer.cs @@ -22,6 +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.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 new file mode 100644 index 000000000..fe627a7c2 --- /dev/null +++ b/test/e2e/Tests/Tests/RestartOrchestrationTests.cs @@ -0,0 +1,154 @@ +// 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 System.Text.Json; +using Xunit; +using Xunit.Abstractions; + +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)] + [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("RestarttOrchestration_HttpStart/SimpleOrchestrator"); + 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 should be the same as input is same. + Assert.Equal(output1, output2); + // Created time should be different. + 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); + } + } + + [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 + [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() + { + 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"); + + string responseContent = await restartResponse.Content.ReadAsStringAsync(); + + // Verfity we weill return the right exception message. + Assert.Contains(fixture.functionLanguageLocalizer.GetLocalizedStringValue("RestartInvalidInstance.ErrorMessage", testInstanceId), responseContent); + } + + [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 + [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("RestarttOrchestration_HttpStart/LongOrchestrator"); + 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.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); + } +}