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);
+ }
+}