-
Notifications
You must be signed in to change notification settings - Fork 9
Rewind instance #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rewind instance #82
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -315,6 +315,64 @@ export class TaskHubGrpcClient { | |||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||
| * Rewinds a failed orchestration instance to a previous state to allow it to retry from the point of failure. | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * This method is used to "rewind" a failed orchestration back to its last known good state, allowing it | ||||||||||||||||||||||||||
| * to be replayed from that point. This is particularly useful for recovering from transient failures | ||||||||||||||||||||||||||
| * or for debugging purposes. | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * Only orchestration instances in the `Failed` state can be rewound. | ||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||
| * @param instanceId - The unique identifier of the orchestration instance to rewind. | ||||||||||||||||||||||||||
| * @param reason - A reason string describing why the orchestration is being rewound. | ||||||||||||||||||||||||||
| * @throws {Error} If the orchestration instance is not found. | ||||||||||||||||||||||||||
| * @throws {Error} If the orchestration instance is in a state that does not allow rewinding. | ||||||||||||||||||||||||||
| * @throws {Error} If the rewind operation is not supported by the backend. | ||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||
| async rewindInstance(instanceId: string, reason: string): Promise<void> { | ||||||||||||||||||||||||||
|
Comment on lines
+328
to
+333
|
||||||||||||||||||||||||||
| * @param reason - A reason string describing why the orchestration is being rewound. | |
| * @throws {Error} If the orchestration instance is not found. | |
| * @throws {Error} If the orchestration instance is in a state that does not allow rewinding. | |
| * @throws {Error} If the rewind operation is not supported by the backend. | |
| */ | |
| async rewindInstance(instanceId: string, reason: string): Promise<void> { | |
| * @param reason - An optional reason string describing why the orchestration is being rewound. | |
| * @throws {Error} If the orchestration instance is not found. | |
| * @throws {Error} If the orchestration instance is in a state that does not allow rewinding. | |
| * @throws {Error} If the rewind operation is not supported by the backend. | |
| */ | |
| async rewindInstance(instanceId: string, reason?: string): Promise<void> { |
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new error-mapping logic (NOT_FOUND / FAILED_PRECONDITION / UNIMPLEMENTED / CANCELLED) is core behavior of this API but isn’t reliably covered by the added E2E tests (positive tests are skipped, and negative tests mostly assert generic toThrow). Add unit tests that mock the gRPC stub to throw errors with these status codes and assert the resulting messages/behavior.
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reason is typed as required (reason: string) but the implementation treats it as optional (if (reason) { ... }). This is confusing for API consumers. Consider changing the signature to reason?: string (and updating the JSDoc accordingly), or enforce a required non-empty reason (and validate it similarly to instanceId).
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Library code shouldn’t console.log() (it’s noisy, hard to suppress in production, and can leak identifiers/reasons into logs). Consider removing this line or using the project’s logging abstraction (e.g., injected logger / debug-level logger) so callers can control logging.
Copilot
AI
Feb 2, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new error-mapping logic (NOT_FOUND / FAILED_PRECONDITION / UNIMPLEMENTED / CANCELLED) is core behavior of this API but isn’t reliably covered by the added E2E tests (positive tests are skipped, and negative tests mostly assert generic toThrow). Add unit tests that mock the gRPC stub to throw errors with these status codes and assert the resulting messages/behavior.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,226 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Licensed under the MIT License. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * E2E tests for rewindInstance API. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * NOTE: These tests can run against either: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 1. DTS emulator - set ENDPOINT and TASKHUB environment variables | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 2. Real Azure DTS - set AZURE_DTS_CONNECTION_STRING environment variable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Example for emulator: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * docker run -i -p 8080:8080 -d mcr.microsoft.com/dts/dts-emulator:latest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * ENDPOINT=localhost:8080 TASKHUB=default npx jest rewind.spec.ts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Example for real DTS: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * AZURE_DTS_CONNECTION_STRING="Endpoint=https://...;Authentication=DefaultAzure;TaskHub=th3" npx jest rewind.spec.ts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TaskHubGrpcClient, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TaskHubGrpcWorker, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| OrchestrationStatus, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| OrchestrationContext, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TOrchestrator, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from "@microsoft/durabletask-js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DurableTaskAzureManagedClientBuilder, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DurableTaskAzureManagedWorkerBuilder, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from "@microsoft/durabletask-js-azuremanaged"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Read environment variables - support both connection string and endpoint/taskhub | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const connectionString = process.env.AZURE_DTS_CONNECTION_STRING; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const endpoint = process.env.ENDPOINT || "localhost:8080"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const taskHub = process.env.TASKHUB || "default"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function createClient(): TaskHubGrpcClient { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const builder = new DurableTaskAzureManagedClientBuilder(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (connectionString) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return builder.connectionString(connectionString).build(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return builder.endpoint(endpoint, taskHub, null).build(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function createWorker(): TaskHubGrpcWorker { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const builder = new DurableTaskAzureManagedWorkerBuilder(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (connectionString) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return builder.connectionString(connectionString).build(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return builder.endpoint(endpoint, taskHub, null).build(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe("Rewind Instance E2E Tests", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let taskHubClient: TaskHubGrpcClient; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let taskHubWorker: TaskHubGrpcWorker; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let workerStarted = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Helper to start worker and track state | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const startWorker = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await taskHubWorker.start(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workerStarted = true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| beforeEach(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workerStarted = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| taskHubClient = createClient(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| taskHubWorker = createWorker(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| afterEach(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (workerStarted) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await taskHubWorker.stop(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await taskHubClient.stop(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe("rewindInstance - positive cases", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Track execution attempt count for retry logic | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let attemptCount = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // An orchestrator that fails on first attempt, succeeds on subsequent attempts | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const failOnceOrchestrator: TOrchestrator = async (_ctx: OrchestrationContext, _input: number) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Use input as a key to track attempts per instance | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // After rewind, the orchestrator replays from the beginning | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| attemptCount++; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (attemptCount === 1) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("First attempt failed!"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return `Success on attempt ${attemptCount}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| beforeEach(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| attemptCount = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+77
to
+92
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Track execution attempt count for retry logic | |
| let attemptCount = 0; | |
| // An orchestrator that fails on first attempt, succeeds on subsequent attempts | |
| const failOnceOrchestrator: TOrchestrator = async (_ctx: OrchestrationContext, _input: number) => { | |
| // Use input as a key to track attempts per instance | |
| // After rewind, the orchestrator replays from the beginning | |
| attemptCount++; | |
| if (attemptCount === 1) { | |
| throw new Error("First attempt failed!"); | |
| } | |
| return `Success on attempt ${attemptCount}`; | |
| }; | |
| beforeEach(() => { | |
| attemptCount = 0; | |
| // Track execution attempt count for retry logic, keyed by input/instance | |
| const attemptCountByInput = new Map<number, number>(); | |
| // An orchestrator that fails on first attempt per input, succeeds on subsequent attempts | |
| const failOnceOrchestrator: TOrchestrator = async (_ctx: OrchestrationContext, _input: number) => { | |
| // Use input as a key to track attempts per instance | |
| // After rewind, the orchestrator replays from the beginning | |
| const previousAttemptCount = attemptCountByInput.get(_input) ?? 0; | |
| const currentAttemptCount = previousAttemptCount + 1; | |
| attemptCountByInput.set(_input, currentAttemptCount); | |
| if (currentAttemptCount === 1) { | |
| throw new Error("First attempt failed!"); | |
| } | |
| return `Success on attempt ${currentAttemptCount}`; | |
| }; | |
| beforeEach(() => { | |
| attemptCountByInput.clear(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The JSDoc describes rewinding to a “previous state” / “last known good state” and replaying “from that point”, which may not match actual backend semantics (many “rewind” implementations restart execution and replay from the beginning/history). Since this is a public API doc, please align the description with the actual server behavior (what state changes, whether execution restarts from the beginning, whether history is preserved, etc.) to avoid misleading users.