Skip to content

Commit fcf15cc

Browse files
committed
feat: Enhance retry mechanism with failure handling and context
1 parent f20505c commit fcf15cc

File tree

13 files changed

+1068
-9
lines changed

13 files changed

+1068
-9
lines changed

packages/durabletask-js/src/index.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,36 @@ export {
6262
// Proto types (for advanced usage)
6363
export { OrchestrationStatus as ProtoOrchestrationStatus } from "./proto/orchestrator_service_pb";
6464

65+
// Failure details
66+
export { FailureDetails, TaskFailureDetails } from "./task/failure-details";
67+
6568
// Task utilities
6669
export { getName, whenAll, whenAny } from "./task";
6770
export { Task } from "./task/task";
6871

6972
// Retry policies and task options
70-
export { RetryPolicy, RetryPolicyOptions } from "./task/retry";
73+
export {
74+
RetryPolicy,
75+
RetryPolicyOptions,
76+
FailureHandlerPredicate,
77+
RetryContext,
78+
createRetryContext,
79+
RetryHandler,
80+
AsyncRetryHandler,
81+
toAsyncRetryHandler,
82+
} from "./task/retry";
7183
export {
7284
TaskOptions,
7385
SubOrchestrationOptions,
7486
StartOrchestrationOptions,
87+
TaskRetryOptions,
7588
taskOptionsFromRetryPolicy,
89+
taskOptionsFromRetryHandler,
90+
taskOptionsFromSyncRetryHandler,
7691
subOrchestrationOptionsFromRetryPolicy,
92+
subOrchestrationOptionsFromRetryHandler,
93+
isRetryPolicy,
94+
isAsyncRetryHandler,
7795
} from "./task/options";
7896

7997
// Types

packages/durabletask-js/src/task/failure-details.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
export class FailureDetails {
4+
/**
5+
* Interface representing task failure details.
6+
* This is used for retry handlers to inspect failure information.
7+
*/
8+
export interface TaskFailureDetails {
9+
/** The type/class name of the error */
10+
readonly errorType: string;
11+
/** The error message */
12+
readonly message: string;
13+
/** The stack trace, if available */
14+
readonly stackTrace?: string;
15+
}
16+
17+
export class FailureDetails implements TaskFailureDetails {
518
private _message: string;
619
private _errorType: string;
720
private _stackTrace: string | undefined;

packages/durabletask-js/src/task/options/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ export {
55
TaskOptions,
66
SubOrchestrationOptions,
77
StartOrchestrationOptions,
8+
TaskRetryOptions,
89
taskOptionsFromRetryPolicy,
10+
taskOptionsFromRetryHandler,
11+
taskOptionsFromSyncRetryHandler,
912
subOrchestrationOptionsFromRetryPolicy,
13+
subOrchestrationOptionsFromRetryHandler,
14+
isRetryPolicy,
15+
isAsyncRetryHandler,
1016
} from "./task-options";

packages/durabletask-js/src/task/options/task-options.ts

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,23 @@
22
// Licensed under the MIT License.
33

44
import { RetryPolicy } from "../retry/retry-policy";
5+
import { AsyncRetryHandler, RetryHandler, toAsyncRetryHandler } from "../retry/retry-handler";
6+
7+
/**
8+
* Union type representing either a RetryPolicy for declarative retry or an AsyncRetryHandler for imperative retry.
9+
*/
10+
export type TaskRetryOptions = RetryPolicy | AsyncRetryHandler;
511

612
/**
713
* Options that can be used to control the behavior of orchestrator task execution.
814
*/
915
export interface TaskOptions {
1016
/**
11-
* The retry policy for the task.
12-
* Controls how many times a task is retried and the delay between retries.
17+
* The retry options for the task.
18+
* Can be either a RetryPolicy for declarative retry control,
19+
* or an AsyncRetryHandler for imperative (programmatic) retry control.
1320
*/
14-
retry?: RetryPolicy;
21+
retry?: TaskRetryOptions;
1522
/**
1623
* The tags to associate with the task.
1724
*/
@@ -81,6 +88,48 @@ export function taskOptionsFromRetryPolicy(policy: RetryPolicy): TaskOptions {
8188
return { retry: policy };
8289
}
8390

91+
/**
92+
* Creates a TaskOptions instance from an AsyncRetryHandler.
93+
*
94+
* @param handler - The async retry handler to use
95+
* @returns A TaskOptions instance configured with the retry handler
96+
*
97+
* @example
98+
* ```typescript
99+
* const handler: AsyncRetryHandler = async (context) => {
100+
* if (context.lastAttemptNumber >= 5) return false;
101+
* if (context.lastFailure.errorType === "ValidationError") return false;
102+
* return true;
103+
* };
104+
*
105+
* const options = taskOptionsFromRetryHandler(handler);
106+
* await ctx.callActivity("myActivity", input, options);
107+
* ```
108+
*/
109+
export function taskOptionsFromRetryHandler(handler: AsyncRetryHandler): TaskOptions {
110+
return { retry: handler };
111+
}
112+
113+
/**
114+
* Creates a TaskOptions instance from a synchronous RetryHandler.
115+
*
116+
* @param handler - The sync retry handler to use (will be wrapped in a Promise)
117+
* @returns A TaskOptions instance configured with the retry handler
118+
*
119+
* @example
120+
* ```typescript
121+
* const handler: RetryHandler = (context) => {
122+
* return context.lastAttemptNumber < 3;
123+
* };
124+
*
125+
* const options = taskOptionsFromSyncRetryHandler(handler);
126+
* await ctx.callActivity("myActivity", input, options);
127+
* ```
128+
*/
129+
export function taskOptionsFromSyncRetryHandler(handler: RetryHandler): TaskOptions {
130+
return { retry: toAsyncRetryHandler(handler) };
131+
}
132+
84133
/**
85134
* Creates a SubOrchestrationOptions instance from a RetryPolicy and optional instance ID.
86135
*
@@ -104,3 +153,46 @@ export function subOrchestrationOptionsFromRetryPolicy(
104153
): SubOrchestrationOptions {
105154
return { retry: policy, instanceId };
106155
}
156+
157+
/**
158+
* Creates a SubOrchestrationOptions instance from an AsyncRetryHandler and optional instance ID.
159+
*
160+
* @param handler - The async retry handler to use
161+
* @param instanceId - Optional instance ID for the sub-orchestration
162+
* @returns A SubOrchestrationOptions instance configured with the retry handler
163+
*
164+
* @example
165+
* ```typescript
166+
* const handler: AsyncRetryHandler = async (context) => {
167+
* return context.lastAttemptNumber < 3;
168+
* };
169+
*
170+
* const options = subOrchestrationOptionsFromRetryHandler(handler, "my-sub-orch-123");
171+
* ```
172+
*/
173+
export function subOrchestrationOptionsFromRetryHandler(
174+
handler: AsyncRetryHandler,
175+
instanceId?: string,
176+
): SubOrchestrationOptions {
177+
return { retry: handler, instanceId };
178+
}
179+
180+
/**
181+
* Type guard to check if the retry option is a RetryPolicy.
182+
*
183+
* @param retry - The retry option to check
184+
* @returns true if the retry option is a RetryPolicy, false otherwise
185+
*/
186+
export function isRetryPolicy(retry: TaskRetryOptions | undefined): retry is RetryPolicy {
187+
return retry instanceof RetryPolicy;
188+
}
189+
190+
/**
191+
* Type guard to check if the retry option is an AsyncRetryHandler.
192+
*
193+
* @param retry - The retry option to check
194+
* @returns true if the retry option is an AsyncRetryHandler, false otherwise
195+
*/
196+
export function isAsyncRetryHandler(retry: TaskRetryOptions | undefined): retry is AsyncRetryHandler {
197+
return typeof retry === "function";
198+
}
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
export { RetryPolicy, RetryPolicyOptions } from "./retry-policy";
4+
export { RetryPolicy, RetryPolicyOptions, FailureHandlerPredicate } from "./retry-policy";
5+
export { RetryContext, createRetryContext } from "./retry-context";
6+
export { RetryHandler, AsyncRetryHandler, toAsyncRetryHandler } from "./retry-handler";
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { TaskFailureDetails } from "../failure-details";
5+
6+
/**
7+
* Retry context data that's provided to task retry handler implementations.
8+
*
9+
* @remarks
10+
* This context is passed to custom retry handlers to provide information about
11+
* the current retry state and allow making informed decisions about whether
12+
* to continue retrying.
13+
*
14+
* Retry handler code is an extension of the orchestrator code and must therefore
15+
* comply with all the determinism requirements of orchestrator code.
16+
*
17+
* @example
18+
* ```typescript
19+
* const retryHandler: RetryHandler = (context: RetryContext) => {
20+
* // Don't retry after 5 attempts
21+
* if (context.lastAttemptNumber >= 5) {
22+
* return false;
23+
* }
24+
* // Don't retry if we've been retrying for more than 5 minutes
25+
* if (context.totalRetryTimeInMilliseconds > 300000) {
26+
* return false;
27+
* }
28+
* // Don't retry certain error types
29+
* if (context.lastFailure.errorType === "ValidationError") {
30+
* return false;
31+
* }
32+
* return true;
33+
* };
34+
* ```
35+
*/
36+
export interface RetryContext {
37+
/**
38+
* The previous retry attempt number.
39+
* This is 1 after the first failure, 2 after the second, etc.
40+
*/
41+
readonly lastAttemptNumber: number;
42+
43+
/**
44+
* The details of the previous task failure.
45+
* Contains the error type, message, and stack trace.
46+
*/
47+
readonly lastFailure: TaskFailureDetails;
48+
49+
/**
50+
* The total amount of time spent in the retry loop for the current task, in milliseconds.
51+
* This includes the time spent executing the task and waiting between retries.
52+
*/
53+
readonly totalRetryTimeInMilliseconds: number;
54+
55+
/**
56+
* Whether the retry operation has been cancelled.
57+
* Handlers should check this and return false if cancellation is requested.
58+
*/
59+
readonly isCancelled: boolean;
60+
}
61+
62+
/**
63+
* Creates a new RetryContext object.
64+
*
65+
* @param lastAttemptNumber - The previous retry attempt number
66+
* @param lastFailure - The details of the previous task failure
67+
* @param totalRetryTimeInMilliseconds - The total time spent retrying
68+
* @param isCancelled - Whether cancellation has been requested
69+
* @returns A RetryContext object
70+
*/
71+
export function createRetryContext(
72+
lastAttemptNumber: number,
73+
lastFailure: TaskFailureDetails,
74+
totalRetryTimeInMilliseconds: number,
75+
isCancelled: boolean = false,
76+
): RetryContext {
77+
return {
78+
lastAttemptNumber,
79+
lastFailure,
80+
totalRetryTimeInMilliseconds,
81+
isCancelled,
82+
};
83+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { RetryContext } from "./retry-context";
5+
6+
/**
7+
* Delegate for manually handling task retries (synchronous version).
8+
*
9+
* @remarks
10+
* Retry handler code is an extension of the orchestrator code and must therefore
11+
* comply with all the determinism requirements of orchestrator code. This means:
12+
* - No I/O operations (file, network, database)
13+
* - No random number generation
14+
* - No accessing current date/time directly
15+
* - No accessing environment variables
16+
*
17+
* @param retryContext - Retry context that's updated between each retry attempt
18+
* @returns Returns `true` to continue retrying or `false` to stop retrying
19+
*
20+
* @example
21+
* ```typescript
22+
* const handler: RetryHandler = (context) => {
23+
* // Retry up to 5 times
24+
* if (context.lastAttemptNumber >= 5) {
25+
* return false;
26+
* }
27+
* // Don't retry validation errors
28+
* if (context.lastFailure.errorType === "ValidationError") {
29+
* return false;
30+
* }
31+
* return true;
32+
* };
33+
*
34+
* const options = taskOptionsFromRetryHandler(handler);
35+
* await ctx.callActivity("myActivity", input, options);
36+
* ```
37+
*/
38+
export type RetryHandler = (retryContext: RetryContext) => boolean;
39+
40+
/**
41+
* Delegate for manually handling task retries (asynchronous version).
42+
*
43+
* @remarks
44+
* Retry handler code is an extension of the orchestrator code and must therefore
45+
* comply with all the determinism requirements of orchestrator code. This means:
46+
* - No I/O operations (file, network, database)
47+
* - No random number generation
48+
* - No accessing current date/time directly
49+
* - No accessing environment variables
50+
*
51+
* While this handler is async, the async operations should only be used for
52+
* deterministic orchestration operations (like waiting for events or timers),
53+
* not for non-deterministic I/O.
54+
*
55+
* @param retryContext - Retry context that's updated between each retry attempt
56+
* @returns Returns a Promise that resolves to `true` to continue retrying or `false` to stop retrying
57+
*
58+
* @example
59+
* ```typescript
60+
* const asyncHandler: AsyncRetryHandler = async (context) => {
61+
* // Retry up to 5 times
62+
* if (context.lastAttemptNumber >= 5) {
63+
* return false;
64+
* }
65+
* // Add exponential backoff
66+
* const delay = Math.min(1000 * Math.pow(2, context.lastAttemptNumber), 30000);
67+
* // Note: In practice you would use ctx.createTimer() for deterministic delays
68+
* return true;
69+
* };
70+
*
71+
* const options = taskOptionsFromRetryHandler(asyncHandler);
72+
* await ctx.callActivity("myActivity", input, options);
73+
* ```
74+
*/
75+
export type AsyncRetryHandler = (retryContext: RetryContext) => Promise<boolean>;
76+
77+
/**
78+
* Creates an AsyncRetryHandler from a synchronous RetryHandler.
79+
*
80+
* @param handler - The synchronous retry handler to wrap
81+
* @returns An AsyncRetryHandler that wraps the synchronous handler
82+
*
83+
* @example
84+
* ```typescript
85+
* const syncHandler: RetryHandler = (context) => context.lastAttemptNumber < 3;
86+
* const asyncHandler = toAsyncRetryHandler(syncHandler);
87+
* ```
88+
*/
89+
export function toAsyncRetryHandler(handler: RetryHandler): AsyncRetryHandler {
90+
return (context: RetryContext) => Promise.resolve(handler(context));
91+
}

0 commit comments

Comments
 (0)