跳到主要内容
当前查看的是 1.35.x。最新版本为 1.45.x。

StepOptions

查看 Markdown

A Step type returns StepOptions to customize its methods. WaitFor and Execute have separate retry policies, timeouts, failure policies, state selections, and lock sets.

Override StepOptions per Step execution​

The StepOptions returned by a Step implementation apply to every Step execution by default. To override them for one Step execution, provide the StepOptions override when a StepDecision goes to the next Steps.

TypeScript
public execute(_context: Context, input: string): StepDecision {
const override: StepOptions = {
waitForRetry: { maximumAttempts: 2 },
waitForFailure: "proceed",
};
const payload = `${input}_state1`;
return goToMany(StepMovement.of(OverrideSecond, payload, override));
}

Example: examples/typescript/src/primitives/options-override/options-override-flow.ts

Retry and failure policies​

  • Timeout limits a single attempt of the method invocation. See Timeouts and total duration.
  • Retry policy re-invokes the same method on the same Step execution until attempts are exhausted.
  • Failure policy decides what happens after retries end. The default fails the Flow. WaitFor can run Execute anyway, and Execute can go to a recovery Step.
TypeScript
public getStepOptions(): StepOptions {
return {
executeRetry: {
initialIntervalMs: 1000,
backoffCoefficient: 2,
maximumAttempts: 5,
},
};
}

Example: examples/typescript/src/primitives/step/retry-flow.ts

WaitFor failure policy​

When WaitFor retries are exhausted:

  • FailFlow fails the Flow. This is the default.
  • PROCEED continues to invoke Execute.

With PROCEED, the same Step's Execute receives the durable input unchanged. waitForMethodFailed() returns true. getRecoveryError() has the error type and detail from the final failed WaitFor attempt.

TypeScript
public getStepOptions(): StepOptions {
return {
waitForRetry: { maximumAttempts: 2 },
waitForFailure: "proceed",
};
}

public waitFor(_context: Context, _input: string): Wait {
throw new Error("planned WaitFor failure");
}

public execute(context: Context, input: string): StepDecision {
if (!context.waitForMethodFailed()) {
throw new Error("waitFor failure was not reported");
}
return goTo(Finish, `${input}_recovered`);
}

Example: examples/typescript/src/primitives/proceed-on-wait-failure/proceed-on-wait-failure-flow.ts

Execute failure policy​

When Execute retries are exhausted, onExecuteFailureProceedTo / ExecuteFailure.proceedTo(...) proceeds to a recovery Step with the same input.

TypeScript
public getStepOptions(): StepOptions {
return {
executeFailure: ExecuteFailure.proceedTo(UpdateQuantityRecovery, {
executeRetry: { maximumAttempts: 5 },
}),
executeRetry: { maximumAttempts: 5 },
};
}

Example: examples/typescript/src/patterns/recovery/failure-recovery-flow.ts

The recovery Step receives the failed Step's durable input unchanged. getRecoveryError() has the error type and detail from the final failed Execute attempt, and its attempt progress. See the Failure Handling pattern and the money-transfer use case.

Last failure info​

  • Terminal events StepExecuteFailed / StepWaitForFailed carry failure output when the Step ultimately fails.
  • During retries, before the failure policy runs, Step events and live Flow state expose lastFailureInfo: the most recent attempt's error type, detail, and Worker metadata.
  • In Dex Web, open event details to see Last failure while retries are in flight. Active Steps in the Flow overview also show lastFailureInfo.

RetryAfter​

Use RetryAfter to choose the delay before the next attempt. It overrides the retry schedule of the RetryPolicy.

Retries recover from failed attempts. Do not use RetryAfter to wait for an external system; use the Polling pattern.

A retry of one Execute call reuses the state snapshot that Dex took when it handed the first attempt to a Worker, so it does not see Attribute writes made after that. To wait for an external write, have the writer also publish a Channel message, directly or from an RPC handler, and wait on that message in the WaitFor of the next Step. Alternatively, the next Step can wait on a Timer and read fresh state when it runs.

TypeScript
if (context.attempt < readyAfterAttempt) {
const cause = new Error(`not ready on attempt ${context.attempt}`);
throw retryAfter(7, cause);
}
return gracefulComplete("ready");

Example: examples/typescript/src/primitives/custom-retry/custom-retry-flow.ts

Durability​

SYNC durability records the Step completion in Dex Server and persists it to storage synchronously. It also makes the attempt start durable in storage before the Worker receives the task.

ASYNC durability does this asynchronously. Dex Server hands the task to the Worker and starts the next work before the start and the completion are persisted, and it batches these commits within about 20 milliseconds. Sustained server throughput is about the same for both modes. ASYNC has lower latency, and a single Flow runs faster. On object storage with low write latency, ASYNC also makes far fewer requests.

If a Dex Server node fails before the commit, a recent ASYNC result is lost. The method runs again, and so does any later work that started from that result. If the node fails before the ASYNC start was persisted, Dex rolls the start back and runs the same attempt number again. This consumes no retry, and the attempt can see different input. Two executions of the same attempt number can overlap, and Step Stream writes can repeat in the same source. Make ASYNC methods and the work they start idempotent.

Choose SYNC for effects that need the tightest guarantees. Choose ASYNC for short Steps where latency matters and a rare rerun is acceptable. See Failure and redelivery for the full comparison.

SYNC is the default. Dex resolves durability in this order: a method override in StepOptions, the Flow's FlowConfig, then SYNC. Dex pins the durability when a method call becomes pending. Changing step_durability in the Flow config affects only method calls scheduled afterward.

Retry policy, method timeout, and heartbeat settings apply to every attempt in both modes.

TypeScript
class SyncWork implements Step<string> {
public readonly inputCodec = stringCodec;

public constructor(private readonly flow: DurabilityFlow) {}

public getStepType(): string {
return "SyncWork";
}

public getStepOptions(): StepOptions {
return { executeDurability: "sync" };
}

public execute(_context: Context, mode: string): StepDecision {
return goTo(FinishDurability, `sync:${mode}`);
}
}

class AsyncWork implements Step<string> {
public readonly inputCodec = stringCodec;

public constructor(private readonly flow: DurabilityFlow) {}

public getStepType(): string {
return "AsyncWork";
}

public getStepOptions(): StepOptions {
return { executeDurability: "async" };
}

public execute(_context: Context, mode: string): StepDecision {
return goTo(FinishDurability, `async:${mode}`);
}
}

Example: examples/typescript/src/primitives/durability/durability-flow.ts

Timeouts and total duration​

Three options bound the time a Step method can take.

LimitOptionDefaultEnforcement
Longest single attemptWaitForMethodTimeout or ExecuteMethodTimeoutTwo hours. The server has no hard cap.Dex times out the attempt. Another attempt starts only while TotalDuration has time left.
Time budget for retriesTotalDuration in the retry policyFour hours, for both durability modes.After an attempt fails, Dex retries only while the budget has time left. It never stops an attempt in progress.
Stalled WorkerHeartbeatTimeoutOne minute. The default allowed minimum is 10 seconds.Dex times out an attempt that sends no heartbeat or Stream message within this window. The retry policy then starts another attempt. See Heartbeat.

The method timeout and the heartbeat timeout start when Dex sends the task to the Worker. Waiting for locks or for a Worker, including time in the task queue, does not count. The deadline in the task is the send time plus the method timeout.

TotalDuration starts when Dex first hands attempt 1 to a Worker. If attempt 1 was returned without running, the clock resets. Dex does not check TotalDuration while a Step waits in the task queue, so a retry that waited past it still runs. A retry waits in the queue again. Dex dispatches queued tasks roughly in the order they became ready, with no strict order. That wait counts toward TotalDuration. After a long backlog, a failed retry may have no time left.

Dex uses TotalDuration only after an attempt fails, to decide whether to retry. It never cuts a running attempt, so the last attempt can start or end after TotalDuration. Only the method timeout limits a single attempt. To bound one long attempt, set ExecuteMethodTimeout or WaitForMethodTimeout, not TotalDuration.

To bound a long wait, set the method timeout and TotalDuration to the same maximum. Dex enforces both, so the Step does not need its own deadline. With Workers available, the total wait is at most about TotalDuration plus one method timeout. Give the retry policy a few attempts with backoff for transient errors and Worker crashes.

When a timeout expires, Dex fails the attempt with a timeout error. After retries end, the failure policy decides what happens next. If no failure route is configured, the Flow fails. See Execute Failure Recovery. For a long wait on an external system, see Polling.

Heartbeat​

During an attempt, the Worker must send a heartbeat or a Stream message before the HeartbeatTimeout expires. Sending a Stream message is an implicit heartbeat.

For example, an Execute method timeout of 60 seconds and a HeartbeatTimeout of 10 seconds lets Dex retry the Step in about 10 seconds instead of waiting for the full 60 seconds. To wait for an external system in one heartbeating Step, use the Polling pattern.

A heartbeat can persist a checkpoint value. The next attempt receives the latest stored value and can resume from it. An explicit heartbeat without a value clears the previous checkpoint. An implicit heartbeat from a Stream message preserves the latest explicit state, including an explicitly cleared state. Checkpoints are lossy; see Heartbeats are not acknowledged.

TypeScript
public getStepOptions(): StepOptions {
return {
executeMethodTimeoutMs: 60_000,
heartbeatTimeoutMs: 10_000,
executeRetry: {
maximumAttempts: 3,
},
};
}

public async execute(context: AsyncContext, batches: number): Promise<StepDecision> {
const completedBatches = context.getLastHeartbeatValue(doubleCodec) ?? 0;
for (let batch = completedBatches; batch < batches; batch++) {
if (context.cancellationSignal.aborted) {
return deadEnd();
}
await new Promise((resolve) => setTimeout(resolve, 2_000));
await context.recordHeartbeat(batch + 1, doubleCodec);
}
return gracefulComplete("processed");
}

Example: examples/typescript/src/primitives/heartbeat/heartbeat-flow.ts

Cancellation​

Dex cancels a running attempt directly. It sends a cancel request to the Worker when the Step is canceled, for example by a StepDecision or an RPC, and when the attempt times out. The SDK also cancels the handler when the attempt deadline in the task passes. The handler does not need to complete, fail, or record a heartbeat to receive cancellation.

Cancellation is cooperative. The SDK cannot stop running code. A long handler must check for cancellation and return, as the heartbeat example above does. See Make the Step stop for the check in each language, and Cancel running Steps for how Dex ends a canceled attempt.

Because cancellation is cooperative, an interrupted execution may still run while the next attempt or the failure route starts.

Selective state loading​

Every WaitFor, Execute, and timeout-handler call receives all ordinary Attributes and size metadata for every Channel and ChannelMap instance. AttributeMap values and pending Channel messages are not loaded unless that method selects them.

StepOptions has separate selections for WaitFor and Execute. Select an entire AttributeMap or ChannelMap when the method must enumerate current instances. Select an exact map instance when its key is already known. An Attribute lock controls concurrency but does not load the AttributeMap value.

The two methods receive independent snapshots. Execute reads its snapshot after the winning Wait conditions consume their messages, so it does not see those consumed messages again. Dex takes the snapshot of attempt 1 when it hands the attempt to a Worker. Retries of one logical method call, and attempts that return to pending, reuse that snapshot.

This Step loads pending queued messages only for Execute. It deletes the first pending message in the same successful handler response.

TypeScript
public getStepOptions(): StepOptions {
return { executeLoadChannels: [queuedMessages] };
}

Example: examples/typescript/src/primitives/channel/channel-flow.ts

Reading an unselected AttributeMap value or pending Channel message returns the SDK's not-loaded error. A selected empty Channel returns an empty message list. Size and map-key introspection remains available without loading message bodies. For Go, dexcli visualize reports a Step method that reads unselected state. See State loads.

Attribute locks​

Concurrent Steps and RPC invocations can read and write the same Attribute. To avoid races, use Attribute locks. Dex Server ensures only one Step execution or RPC handling can acquire the lock at a time. WaitFor and Execute have separate lock sets. Set them by returning StepOptions.

See Attribute locks for when a Step attempt takes and holds its locks, with WaitFor and Execute examples. See Locks for the lock model.