Support status-based orchestration ID reuse policy - #352
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds first-class support for orchestration instance ID reuse by introducing a public OrchestrationIdReusePolicy (status-based deduplication) and plumbing it through the core gRPC client, in-memory testing backend, and the azure-functions-durable wrapper. It also completes CANCELED status mappings end-to-end so runtime status handling stays accurate when applying reuse policies.
Changes:
- Add
OrchestrationIdReusePolicyand serialize it ontoCreateInstanceRequest.orchestrationIdReusePolicy.replaceableStatus(via complement ofdedupeStatuses). - Enable atomic “replace existing instance” behavior in the in-memory backend and forward the policy through
DurableFunctionsClient.startNew(). - Add
OrchestrationStatus.CANCELEDand update status conversions/tests accordingly.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents how to use the new orchestration ID reuse policy. |
| packages/durabletask-js/test/orchestration-id-reuse-policy.spec.ts | Adds unit tests for request serialization and in-memory replacement semantics. |
| packages/durabletask-js/src/testing/test-client.ts | Extends scheduleNewOrchestration overloads to accept StartOrchestrationOptions including reuse policy. |
| packages/durabletask-js/src/testing/in-memory-backend.ts | Implements duplicate-ID policy handling and waiter/timer cleanup during replacement; adds canceled mapping. |
| packages/durabletask-js/src/task/options/task-options.ts | Adds orchestrationIdReusePolicy to StartOrchestrationOptions. |
| packages/durabletask-js/src/orchestration/orchestration-id-reuse-policy.ts | Introduces the public policy type and wire conversion helper. |
| packages/durabletask-js/src/orchestration/enum/orchestration-status.enum.ts | Adds CANCELED to the public orchestration status enum. |
| packages/durabletask-js/src/index.ts | Exports OrchestrationIdReusePolicy from the public surface. |
| packages/durabletask-js/src/client/client.ts | Forwards the new policy into the gRPC CreateInstanceRequest. |
| packages/azure-functions-durable/test/unit/query-types.spec.ts | Adds tests ensuring canceled vs terminated status distinctions are preserved. |
| packages/azure-functions-durable/test/unit/client.spec.ts | Adds a test verifying startNew() forwards the reuse policy to core scheduling. |
| packages/azure-functions-durable/src/orchestration-status.ts | Updates runtime status conversions to include CANCELED. |
| packages/azure-functions-durable/src/index.ts | Re-exports the new core types and StartNewOptions. |
| packages/azure-functions-durable/src/client.ts | Adds orchestrationIdReusePolicy to StartNewOptions and forwards it. |
| packages/azure-functions-durable/README.md | Updates docs to mention orchestrationIdReusePolicy support in startNew(). |
| packages/azure-functions-durable/CHANGELOG.md | Notes forwarding of reuse policy through startNew(). |
| CHANGELOG.md | Notes new reuse policy API and the new CANCELED status member. |
Suppressed comments (1)
packages/durabletask-js/src/testing/in-memory-backend.ts:592
isTerminalStatusdoes not treatORCHESTRATION_STATUS_CANCELEDas terminal. This can cause canceled instances to never be recognized as completed (e.g., waiters may hang or state transitions may behave incorrectly) even thoughtoClientStatusnow exposesCANCELED.
return (
status === pb.OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED ||
status === pb.OrchestrationStatus.ORCHESTRATION_STATUS_FAILED ||
status === pb.OrchestrationStatus.ORCHESTRATION_STATUS_TERMINATED
);
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
packages/durabletask-js/src/testing/test-client.ts:54
- The overloads accept
StartOrchestrationOptions, which includestags/version, but this client throws at runtime when those are provided. Narrowing the overload type to exclude unsupported fields avoids a compile-time/runtime mismatch and guides callers to the supported surface area.
async scheduleNewOrchestration(
orchestrator: TOrchestrator | string,
input?: TInput,
options?: StartOrchestrationOptions,
): Promise<string>;
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.
Suppressed comments (1)
packages/durabletask-js/src/testing/in-memory-backend.ts:113
createInstancedecides whether an existing instance is replaceable by converting the protobuf status viatoClientStatus(), which intentionally mapsCONTINUED_AS_NEWtoRUNNING. This can make a transientCONTINUED_AS_NEWgeneration replaceable wheneverRUNNINGis not indedupeStatuses, contradicting the public contract/docs thatCONTINUED_AS_NEWis not replaceable.
Handle ORCHESTRATION_STATUS_CONTINUED_AS_NEW explicitly as a non-replaceable status before applying the reuse policy.
const existingInstance = this.instances.get(instanceId);
if (existingInstance) {
const existingStatus = this.toClientStatus(existingInstance.status);
if (!orchestrationIdReusePolicy || orchestrationIdReusePolicy.dedupeStatuses.includes(existingStatus)) {
throw new Error(`Orchestration instance '${instanceId}' already exists`);
}
Expose status-based duplicate handling across the core and Azure Functions clients, including faithful in-memory test semantics. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Reject unsupported test-client scheduling metadata, document the protocol's missing atomic IGNORE action, and keep canceled status terminal across testing and export-history surfaces. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Notify a live parent when its child is replaced and cancel child-keyed watchers when their owning parent execution is replaced. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6157e6cd-9ce9-4636-a8d5-4f49f4f32de8
71ac676 to
13f4208
Compare
Expose flat dedupe statuses, validate and serialize the status complement, map duplicate errors, and align in-memory replacement semantics. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d491ec0d-9ce9-421b-9953-7d179d53625b
Keep production gRPC validation wire-focused while applying shim-specific replacement checks and omitted-policy semantics only to the in-memory client. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d491ec0d-9ce9-421b-9953-7d179d53625b
Use the existing connection-string contract so the reuse-policy scenarios run against either authenticated Azure DTS or the local emulator. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d491ec0d-9ce9-421b-9953-7d179d53625b
Cover the service-side rejection of self-conflicting status policies while preserving production request forwarding and the original suspended instance. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d491ec0d-9ce9-421b-9953-7d179d53625b
Include the orchestration ID reuse policy spec in the existing orchestration emulator group for both supported Node versions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d491ec0d-9ce9-421b-9953-7d179d53625b
Allow exact-head emulator verification without changing the existing push, pull request, or matrix coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6157e6cd-9ce9-4636-a8d5-4f49f4f32de8
Summary
StartOrchestrationOptions.dedupeStatusesAPI matching the merged .NET contract and serialize its complement intoCreateInstanceRequest.orchestrationIdReusePolicy.replaceableStatus[]explicitly replace every supported status, exportValidDedupeStatuses, and validate unsupported statusesALREADY_EXISTStoOrchestrationAlreadyExistsErrorandINVALID_ARGUMENTtoTypeError, with equivalent in-memory duplicate errorsazure-functions-durableand cover it against a real Azure DTS Consumption schedulerAPI rationale and scope
The public shape mirrors merged durabletask-dotnet:
dedupeStatusesis a top-level start option, not a nested policy object. Object literals already provide the JavaScript equivalent of the .NETWithDedupeStatusesrecord extension, so no builder helper is added.Production gRPC serialization preserves three states exactly:
undefined: omit the protobuf policy and preserve backend duplicate-ID defaults[]: send all seven valid statuses as replaceableValidDedupeStatuses - dedupeStatusesLike .NET
GrpcDurableTaskClient, the production client validates only that each status is supported and forwards Terminated plus reusable-running combinations to the sidecar. Live DTS rejects that self-conflicting combination withINVALID_ARGUMENT, which the JS client maps toTypeError. Like .NETShimDurableTaskClient, the in-memory client performs the same validation locally and treats omitteddedupeStatusesas all statuses reusable.The canonical shared protocol reserves field 2 and contains only
replaceableStatus; it cannot represent atomic no-op/IGNORE. This PR does not fakeIGNOREwith a racy client-side preflight, so that part of issue #30 remains protocol-blocked.Compatibility
orchestrationIdReusePolicyproposal is cleanly replaced rather than retained as a compatibility aliasscheduleNewOrchestration(..., instanceId, startAt)calls remain supporteddedupeStatusesis omitted; live DTS empirically defaults to duplicate rejectiondedupeStatusesis omitted; this is an observable testing/production divergence inherited from the .NET client splitversionandtagsoptions instead of silently dropping themCANCELEDis mapped and treated as terminal across affected SDK, provider, testing, and export-history surfacesCANCELLEDerror as its rawServiceErrorwith status code intactValidation
azure-functions-durable: 19 suites, 164 tests passedazure-functions-durable, and export-history builds passedgit diff --checkpassedINVALID_ARGUMENT; the client surfacedTypeErrorand the original suspended instance remained untouchedThe broader Azure-managed E2E command also connected successfully and passed
orchestration.spec.ts, but exceeded the local command budget before completing all suites. The feature-specific suite was run directly and passed in full.Refs #30