Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fcf15cc
feat: Enhance retry mechanism with failure handling and context
YunchuWang Feb 5, 2026
b05cd0f
feat: Implement advanced retry features with handleFailure predicate …
YunchuWang Feb 6, 2026
157f357
feat: Implement AsyncRetryHandler and refactor retry task handling
YunchuWang Feb 6, 2026
93d2af8
feat: Refactor taskType to use RetryTaskType in RetryHandlerTask and …
YunchuWang Feb 6, 2026
eceb158
feat: Add E2E tests for AsyncRetryHandler with activity and sub-orche…
YunchuWang Feb 6, 2026
957c0b8
Merge branch 'main' into wangbill/retryhandler
YunchuWang Feb 6, 2026
c34b2ea
Update packages/durabletask-js/src/task/retry/retry-policy.ts
YunchuWang Feb 6, 2026
8ebd6af
Update packages/durabletask-js/src/task/retry/retry-handler.ts
YunchuWang Feb 6, 2026
6f5cb2f
feat: Add taskName getter to RetryTaskBase and log retry attempts in …
YunchuWang Feb 6, 2026
554ba1e
Merge branch 'wangbill/retryhandler' of https://github.com/microsoft/…
YunchuWang Feb 6, 2026
3e10cdb
Update packages/durabletask-js/src/task/retry/retry-policy.ts
YunchuWang Feb 6, 2026
522996e
feat: Enhance retry handler functionality with orchestration context …
YunchuWang Feb 6, 2026
4c4a906
Merge branch 'wangbill/retryhandler' of https://github.com/microsoft/…
YunchuWang Feb 6, 2026
4293820
fix unit tests
YunchuWang Feb 6, 2026
4dda8a8
fix: Preserve custom error names in failure details and adjust retry …
YunchuWang Feb 6, 2026
c8e8fd2
refactor: Defer exception creation in recordFailure method and adjust…
YunchuWang Feb 6, 2026
7a02ceb
refactor: Remove isCancelled property from RetryContext and related t…
YunchuWang Feb 6, 2026
154f118
refactor: Remove unused retry handler options and related tests
YunchuWang Feb 7, 2026
39a30cc
fix: Ensure non-null handler in RetryHandlerTask constructor
YunchuWang Feb 7, 2026
f2d0287
feat: Enhance retry handling with support for delay in milliseconds a…
YunchuWang Feb 7, 2026
47c011a
feat: Add updateAction method to RetryTaskBase and synchronize action…
YunchuWang Feb 7, 2026
a133620
feat: Enhance retry handler to support returning delays and add compr…
YunchuWang Feb 7, 2026
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
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 17 additions & 3 deletions packages/durabletask-js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,18 +62,32 @@ export {
// Proto types (for advanced usage)
export { OrchestrationStatus as ProtoOrchestrationStatus } from "./proto/orchestrator_service_pb";

// Failure details
export { FailureDetails, TaskFailureDetails } from "./task/failure-details";

// Task utilities
export { getName, whenAll, whenAny } from "./task";
export { Task } from "./task/task";

// Retry policies and task options
export { RetryPolicy, RetryPolicyOptions } from "./task/retry";
export {
RetryPolicy,
RetryPolicyOptions,
FailureHandlerPredicate,
RetryContext,
createRetryContext,
RetryHandler,
AsyncRetryHandler,
RetryHandlerResult,
toAsyncRetryHandler,
} from "./task/retry";
export {
TaskOptions,
SubOrchestrationOptions,
StartOrchestrationOptions,
taskOptionsFromRetryPolicy,
subOrchestrationOptionsFromRetryPolicy,
TaskRetryOptions,
isRetryPolicy,
isRetryHandler,
} from "./task/options";

// Types
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export class TaskFailedError extends Error {

constructor(message: string, details: pb.TaskFailureDetails) {
super(message);
this.name = "TaskFailedError";

this._details = new FailureDetails(
details.getErrormessage(),
Expand Down
15 changes: 14 additions & 1 deletion packages/durabletask-js/src/task/failure-details.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

export class FailureDetails {
/**
* Interface representing task failure details.
* This is used for retry handlers to inspect failure information.
*/
export interface TaskFailureDetails {
/** The type/class name of the error */
readonly errorType: string;
/** The error message */
readonly message: string;
/** The stack trace, if available */
readonly stackTrace?: string;
}

export class FailureDetails implements TaskFailureDetails {
private _message: string;
private _errorType: string;
private _stackTrace: string | undefined;
Expand Down
5 changes: 3 additions & 2 deletions packages/durabletask-js/src/task/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export {
TaskOptions,
SubOrchestrationOptions,
StartOrchestrationOptions,
taskOptionsFromRetryPolicy,
subOrchestrationOptionsFromRetryPolicy,
TaskRetryOptions,
isRetryPolicy,
isRetryHandler,
} from "./task-options";
66 changes: 29 additions & 37 deletions packages/durabletask-js/src/task/options/task-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,31 @@
// Licensed under the MIT License.

import { RetryPolicy } from "../retry/retry-policy";
import { AsyncRetryHandler, RetryHandler } from "../retry/retry-handler";

/**
* Union type representing the available retry strategies for a task.
*
* - {@link RetryPolicy} for declarative retry control (with backoff, max attempts, etc.)
* - {@link AsyncRetryHandler} for asynchronous imperative retry control
* - {@link RetryHandler} for synchronous imperative retry control
*
* When a synchronous {@link RetryHandler} is provided, it is automatically
* wrapped into an {@link AsyncRetryHandler} internally.
*/
export type TaskRetryOptions = RetryPolicy | AsyncRetryHandler | RetryHandler;

/**
* Options that can be used to control the behavior of orchestrator task execution.
*/
export interface TaskOptions {
/**
* The retry policy for the task.
* Controls how many times a task is retried and the delay between retries.
* The retry options for the task.
* Can be a RetryPolicy for declarative retry control,
* an AsyncRetryHandler for async imperative retry control,
* or a RetryHandler for sync imperative retry control.
*/
retry?: RetryPolicy;
retry?: TaskRetryOptions;
/**
* The tags to associate with the task.
*/
Expand Down Expand Up @@ -62,45 +77,22 @@ export interface StartOrchestrationOptions {
}

/**
* Creates a TaskOptions instance from a RetryPolicy.
* Type guard to check if the retry option is a RetryPolicy.
*
* @param policy - The retry policy to use
* @returns A TaskOptions instance configured with the retry policy
*
* @example
* ```typescript
* const retryPolicy = new RetryPolicy({
* maxNumberOfAttempts: 3,
* firstRetryIntervalInMilliseconds: 1000
* });
*
* const options = taskOptionsFromRetryPolicy(retryPolicy);
* ```
* @param retry - The retry option to check
* @returns true if the retry option is a RetryPolicy, false otherwise
*/
export function taskOptionsFromRetryPolicy(policy: RetryPolicy): TaskOptions {
return { retry: policy };
export function isRetryPolicy(retry: TaskRetryOptions | undefined): retry is RetryPolicy {
return retry instanceof RetryPolicy;
}

/**
* Creates a SubOrchestrationOptions instance from a RetryPolicy and optional instance ID.
*
* @param policy - The retry policy to use
* @param instanceId - Optional instance ID for the sub-orchestration
* @returns A SubOrchestrationOptions instance configured with the retry policy
*
* @example
* ```typescript
* const retryPolicy = new RetryPolicy({
* maxNumberOfAttempts: 3,
* firstRetryIntervalInMilliseconds: 1000
* });
* Type guard to check if the retry option is a retry handler function
* (either {@link AsyncRetryHandler} or {@link RetryHandler}).
*
* const options = subOrchestrationOptionsFromRetryPolicy(retryPolicy, "my-sub-orch-123");
* ```
* @param retry - The retry option to check
* @returns true if the retry option is a handler function, false otherwise
*/
export function subOrchestrationOptionsFromRetryPolicy(
policy: RetryPolicy,
instanceId?: string,
): SubOrchestrationOptions {
return { retry: policy, instanceId };
export function isRetryHandler(retry: TaskRetryOptions | undefined): retry is AsyncRetryHandler | RetryHandler {
return typeof retry === "function";
}
Comment thread
YunchuWang marked this conversation as resolved.
93 changes: 93 additions & 0 deletions packages/durabletask-js/src/task/retry-handler-task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import * as pb from "../proto/orchestrator_service_pb";
import { OrchestrationContext } from "./context/orchestration-context";
import { RetryTaskBase, RetryTaskType } from "./retry-task-base";
import { AsyncRetryHandler, RetryHandlerResult } from "./retry/retry-handler";
import { createRetryContext } from "./retry/retry-context";
import { TaskFailureDetails } from "./failure-details";

/**
* A task that uses an AsyncRetryHandler for imperative retry control.
*
* @remarks
* Unlike RetryableTask which uses a declarative RetryPolicy, this task delegates
* all retry decisions to a user-provided handler function. The handler receives
* a RetryContext with failure details, attempt count, and elapsed time, and
* returns true to retry or false to stop.
*
* This mirrors the .NET SDK's InvokeWithCustomRetryHandler pattern, where the
* retry handler runs as orchestrator code (subject to replay).
*/
export class RetryHandlerTask<T> extends RetryTaskBase<T> {
private readonly _handler: AsyncRetryHandler;
private readonly _orchestrationContext: OrchestrationContext;

/**
* Creates a new RetryHandlerTask instance.
*
* @param handler - The async retry handler for imperative retry decisions
* @param orchestrationContext - The orchestration context for the current execution
* @param action - The orchestrator action associated with this task
* @param startTime - The time when the task was first scheduled
* @param taskType - The type of task (activity or sub-orchestration)
*/
constructor(
handler: AsyncRetryHandler,
orchestrationContext: OrchestrationContext,
action: pb.OrchestratorAction,
startTime: Date,
taskType: RetryTaskType,
) {
super(action, startTime, taskType);
if (!handler) {
throw new Error("RetryHandlerTask requires a non-null handler");
}
this._handler = handler;
this._orchestrationContext = orchestrationContext;
}

/**
* Gets the async retry handler for this task.
Comment thread
YunchuWang marked this conversation as resolved.
*/
get handler(): AsyncRetryHandler {
return this._handler;
}

/**
* Invokes the async retry handler to determine whether to retry.
*
* @param currentTime - The current orchestration time (for deterministic replay)
* @returns A Promise that resolves to `true` to retry immediately,
* `false` to stop retrying, or a positive number indicating the
* delay in milliseconds before the next retry attempt
*/
async shouldRetry(currentTime: Date): Promise<RetryHandlerResult> {
if (!this.lastFailure) {
return false;
}

// Check for non-retriable failures (e.g., activity not found)
if (this.lastFailure.getIsnonretriable()) {
return false;
}

const failureDetails: TaskFailureDetails = {
errorType: this.lastFailure.getErrortype() || "Error",
message: this.lastFailure.getErrormessage() || "",
stackTrace: this.lastFailure.getStacktrace()?.getValue(),
};

const totalRetryTimeMs = currentTime.getTime() - this.startTime.getTime();

const retryContext = createRetryContext(
this._orchestrationContext,
this.attemptCount,
failureDetails,
totalRetryTimeMs,
);

Comment thread
YunchuWang marked this conversation as resolved.
return this._handler(retryContext);
}
}
Loading
Loading