Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

### New

- Add `restartOrchestration(id, { newInstanceId, version? })` for restart-as-clone with a required
target ID. Omitted versions preserve backend restart defaults; `""` selects unversioned execution.
Boolean restart calls remain unchanged. Target/version support requires the updated backend protocol.
- Add opt-in `payloads` storage to gRPC clients/workers, with transparent UTF-8-size-based
externalization and awaited query/history/work-item hydration across orchestration, activity,
and entity V1/V2 payloads. Add the optional `@microsoft/durabletask-js-azureblob` storage package.
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,27 @@ Azure Functions `app.orchestration` / `app.activity` remain host registrations w
names, not a multi-version host-routing API. The embedded `DurableFunctionsWorker` inherits core
version dispatch and child defaults for integrations that supply versioned protobuf requests.

### Restart as clone

Restart an existing orchestration from the beginning with its original name and input, not its history:

```typescript
await client.restartOrchestration("order-123", {
newInstanceId: "order-123-retry",
version: "2.0",
});
```

The options form requires a non-empty `newInstanceId`. Omitting `version` preserves the backend's
restart defaults (not the client's default version); `version: ""` selects an unversioned orchestration.
Existing `restartOrchestration(id, boolean)` calls are unchanged.

With backend lineage support, the clone sees `ctx.instanceId === "order-123-retry"` and
`ctx.sourceInstanceId === "order-123"`, including during replay. Cloning the clone again references
`"order-123-retry"` as its immediate source. Sub-orchestrations do not inherit this source reference.
Explicit target IDs and version overrides require a backend supporting the updated restart protocol;
older backends may ignore these fields.

### Continue as new

Long-running orchestrations can restart with fresh history and optionally move to a new
Expand Down
2 changes: 1 addition & 1 deletion internal/protocol/SOURCE_COMMIT
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3145f9337fca9de57d2f89a6ff6f07150d34f1c2
962a493037e72b5e08f07a55eff0f121c1aaae80
14 changes: 14 additions & 0 deletions internal/protocol/protos/orchestrator_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -533,9 +533,22 @@ message PurgeInstancesResponse {
google.protobuf.BoolValue isComplete = 2;
}

// Restarts an orchestration from the beginning with its original name and input,
// without copying its history.
message RestartInstanceRequest {
string instanceId = 1;

// When newInstanceId is absent, true generates a new instance ID and false
// restarts using instanceId.
bool restartWithNewInstanceId = 2;

// An explicit target instance ID. When present, takes precedence over
// restartWithNewInstanceId.
google.protobuf.StringValue newInstanceId = 3;

// Overrides the restarted orchestration's version, including an empty string.
// When absent, preserves the server's default restart behavior.
google.protobuf.StringValue version = 4;
}

message RestartInstanceResponse {
Expand Down Expand Up @@ -706,6 +719,7 @@ message StartNewOrchestrationAction {
google.protobuf.Timestamp scheduledTime = 5;
google.protobuf.Timestamp requestTime = 6;
TraceContext parentTraceContext = 7;
map<string, string> tags = 8;
}

message AbandonActivityTaskRequest {
Expand Down
4 changes: 3 additions & 1 deletion packages/azure-functions-durable/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
- Expose readonly `context.df.sourceInstanceId`, forwarding core clone lineage on
initial execution and replay. Null or missing metadata returns `undefined`.
Actual Functions lineage requires extension lineage support and provider tag persistence.
Restart APIs remain unchanged; destination-ID/version options are not supported by the protocol.
- Add `client.restart(id, { newInstanceId, version? })`, forwarding required-target cloning options
to core. Omitted versions preserve backend restart defaults; `""` selects unversioned execution.
Existing boolean calls remain supported. Target/version options require an updated backend.
- Add options-object overloads to `callSubOrchestrator()` and `callSubOrchestratorWithRetry()` so
classic orchestrators can attach tags to child orchestration instances. Existing positional
`instanceId` and `version` calls are unchanged.
Expand Down
15 changes: 14 additions & 1 deletion packages/azure-functions-durable/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,20 @@ are `@deprecated`):
| `client.suspend(id)` | `client.suspendOrchestration(id)` |
| `client.resume(id)` | `client.resumeOrchestration(id)` |
| `client.rewind(id, reason)` | `client.rewindInstance(id, reason)` |
| `client.restart(id, restartWithNewInstanceId?)` | `client.restartOrchestration(id, restartWithNewInstanceId?)` |
| `client.restart(id, booleanOrOptions?)` | `client.restartOrchestration(id, booleanOrOptions?)` |

To restart as a clone with a chosen ID:

```typescript
await client.restart("order-123", { newInstanceId: "order-123-retry", version: "2.0" });
```

The options form requires `newInstanceId`. Omit `version` to retain backend restart defaults;
use `""` for unversioned execution. Existing boolean calls are unchanged. The clone starts with
the original name/input and fresh history. With backend lineage support,
`context.df.sourceInstanceId` is the immediate source ID on execution and replay; sub-orchestrations
do not inherit it. Target/version options require an updated backend; Functions lineage additionally
requires extension lineage support and provider tag persistence.

## Status

Expand Down
6 changes: 5 additions & 1 deletion packages/azure-functions-durable/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
OrchestrationState,
OrchestrationStatus,
PurgeInstanceCriteria,
RestartOrchestrationOptions,
TaskHubGrpcClient,
} from "@microsoft/durabletask-js";
import { HttpManagementPayload, createHttpManagementPayload as createPayload } from "./http-management-payload";
Expand Down Expand Up @@ -80,6 +81,9 @@ export interface StartNewOptions {
dedupeStatuses?: readonly OrchestrationStatus[];
}

/** Options for {@link DurableFunctionsClient.restart} with a chosen target ID. */
export type RestartOptions = RestartOrchestrationOptions;

/**
* @hidden
* Removes the input/result payload fields from a core history event, honoring v3's
Expand Down Expand Up @@ -559,7 +563,7 @@ export class DurableFunctionsClient extends TaskHubGrpcClient {
* @param restartWithNewInstanceId - When `true`, the restarted instance is assigned a new ID.
* @returns The instance ID of the restarted orchestration.
*/
async restart(instanceId: string, restartWithNewInstanceId = false): Promise<string> {
async restart(instanceId: string, restartWithNewInstanceId: boolean | RestartOptions = false): Promise<string> {
return this.restartOrchestration(instanceId, restartWithNewInstanceId);
}

Expand Down
1 change: 1 addition & 0 deletions packages/azure-functions-durable/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export {
DurableFunctionsClientInput,
DurableOrchestrationClient,
StartNewOptions,
RestartOptions,
TaskHubOptions,
getGrpcHostAddress,
} from "./client";
Expand Down
7 changes: 7 additions & 0 deletions packages/azure-functions-durable/test/unit/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ describe("DurableFunctionsClient", () => {
const restart = jest.spyOn(client, "restartOrchestration").mockResolvedValue("id-2");
await expect(client.restart("id-1", true)).resolves.toBe("id-2");
expect(restart).toHaveBeenCalledWith("id-1", true);
await expect(client.restart("id-1")).resolves.toBe("id-2");
expect(restart).toHaveBeenLastCalledWith("id-1", false);
for (const version of [undefined, "", "2.0"]) {
const options = { newInstanceId: "id-2", version };
await expect(client.restart("id-1", options)).resolves.toBe("id-2");
expect(restart).toHaveBeenLastCalledWith("id-1", options);
}

const purge = jest.spyOn(client, "purgeOrchestration").mockResolvedValue(undefined);
const purgeResult = await client.purgeInstanceHistoryBy({
Expand Down
35 changes: 30 additions & 5 deletions packages/durabletask-js/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { convertFailureDetails } from "../utils/failure-details.util";
import { HistoryEvent } from "../orchestration/history-event";
import { convertProtoHistoryEvent } from "../utils/history-event-converter";
import { Logger, ConsoleLogger } from "../types/logger.type";
import { StartOrchestrationOptions } from "../task/options";
import { StartOrchestrationOptions, RestartOrchestrationOptions } from "../task/options";
import { toProtobufOrchestrationIdReusePolicy } from "../orchestration/orchestration-id-reuse-policy";
import { OrchestrationAlreadyExistsError } from "../orchestration/exception/orchestration-already-exists-error";
import { mapToRecord } from "../utils/tags.util";
Expand Down Expand Up @@ -823,30 +823,55 @@ export class TaskHubGrpcClient {
*
* Restarting with a new ID is a restart-as-clone, not execution-history cloning.
* The context's `sourceInstanceId` is available only if the backend supplies
* lineage metadata. The current protocol does not support choosing a destination ID or version.
* lineage metadata. Pass options to choose a target ID and optionally override the version.
* Omitted versions preserve backend restart defaults; an empty string selects an unversioned orchestration.
* Explicit target IDs and version overrides require a backend supporting these protocol fields.
*
* @param instanceId - The unique ID of the orchestration instance to restart.
* @param restartWithNewInstanceId - If true, the restarted orchestration will be assigned
* a new instance ID. If false (default), the same instance ID will be reused.
* When reusing the same instance ID, the orchestration must be in a terminal state
* (Completed, Failed, or Terminated).
* Alternatively, pass options with a required `newInstanceId` and optional `version`.
* @returns A Promise that resolves to the instance ID of the restarted orchestration.
* This will be the same as the input instanceId if restartWithNewInstanceId is false,
* or a new ID if restartWithNewInstanceId is true.
* @throws Error if the orchestration instance is not found.
* @throws Error if the orchestration cannot be restarted (e.g., it's still running
* and restartWithNewInstanceId is false).
*/
async restartOrchestration(instanceId: string, restartWithNewInstanceId: boolean = false): Promise<string> {
async restartOrchestration(
instanceId: string,
restartWithNewInstanceId: boolean | RestartOrchestrationOptions = false,
): Promise<string> {
if (!instanceId) {
throw new Error("instanceId cannot be null or empty");
}

const req = new pb.RestartInstanceRequest();
req.setInstanceid(instanceId);
req.setRestartwithnewinstanceid(restartWithNewInstanceId);
if (typeof restartWithNewInstanceId === "boolean") {
req.setRestartwithnewinstanceid(restartWithNewInstanceId);
} else {
if (
!restartWithNewInstanceId ||
typeof restartWithNewInstanceId.newInstanceId !== "string" ||
!restartWithNewInstanceId.newInstanceId
) {
throw new TypeError("newInstanceId is required and must be a non-empty string");
}
const { newInstanceId, version } = restartWithNewInstanceId;
if (version !== undefined && typeof version !== "string") {
throw new TypeError("version must be a string");
}
req.setRestartwithnewinstanceid(true);
req.setNewinstanceid(new StringValue().setValue(newInstanceId));
if (version !== undefined) {
req.setVersion(new StringValue().setValue(version));
}
}

ClientLogs.restartingInstance(this._logger, instanceId, restartWithNewInstanceId);
ClientLogs.restartingInstance(this._logger, instanceId, req.getRestartwithnewinstanceid());

const res = await this._call<pb.RestartInstanceRequest, pb.RestartInstanceResponse>(
this._stub.restartInstance.bind(this._stub),
Expand Down
1 change: 1 addition & 0 deletions packages/durabletask-js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export {
TaskOptions,
SubOrchestrationOptions,
StartOrchestrationOptions,
RestartOrchestrationOptions,
TaskRetryOptions,
isRetryPolicy,
isRetryHandler,
Expand Down
17 changes: 17 additions & 0 deletions packages/durabletask-js/src/proto/orchestrator_service_pb.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2647,6 +2647,16 @@ export class RestartInstanceRequest extends jspb.Message {
getRestartwithnewinstanceid(): boolean;
setRestartwithnewinstanceid(value: boolean): RestartInstanceRequest;

hasNewinstanceid(): boolean;
clearNewinstanceid(): void;
getNewinstanceid(): google_protobuf_wrappers_pb.StringValue | undefined;
setNewinstanceid(value?: google_protobuf_wrappers_pb.StringValue): RestartInstanceRequest;

hasVersion(): boolean;
clearVersion(): void;
getVersion(): google_protobuf_wrappers_pb.StringValue | undefined;
setVersion(value?: google_protobuf_wrappers_pb.StringValue): RestartInstanceRequest;

serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): RestartInstanceRequest.AsObject;
static toObject(includeInstance: boolean, msg: RestartInstanceRequest): RestartInstanceRequest.AsObject;
Expand All @@ -2661,6 +2671,8 @@ export namespace RestartInstanceRequest {
export type AsObject = {
instanceid: string,
restartwithnewinstanceid: boolean,
newinstanceid?: google_protobuf_wrappers_pb.StringValue.AsObject,
version?: google_protobuf_wrappers_pb.StringValue.AsObject,
}
}

Expand Down Expand Up @@ -3505,6 +3517,9 @@ export class StartNewOrchestrationAction extends jspb.Message {
getParenttracecontext(): TraceContext | undefined;
setParenttracecontext(value?: TraceContext): StartNewOrchestrationAction;

getTagsMap(): jspb.Map<string, string>;
clearTagsMap(): void;

serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): StartNewOrchestrationAction.AsObject;
static toObject(includeInstance: boolean, msg: StartNewOrchestrationAction): StartNewOrchestrationAction.AsObject;
Expand All @@ -3524,6 +3539,8 @@ export namespace StartNewOrchestrationAction {
scheduledtime?: google_protobuf_timestamp_pb.Timestamp.AsObject,
requesttime?: google_protobuf_timestamp_pb.Timestamp.AsObject,
parenttracecontext?: TraceContext.AsObject,

tagsMap: Array<[string, string]>,
}
}

Expand Down
Loading
Loading