Skip to content

Commit 155c5b8

Browse files
embano1anthonyting
andauthored
docs: update sdk readme with step semantics (#407)
Closes: #406 *Issue #, if available:* *Description of changes:* By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --------- Signed-off-by: Michael Gasch <15986659+embano1@users.noreply.github.com> Co-authored-by: anthonyting <49772744+anthonyting@users.noreply.github.com>
1 parent b804a27 commit 155c5b8

File tree

3 files changed

+87
-5
lines changed

3 files changed

+87
-5
lines changed

packages/aws-durable-execution-sdk-js/README.md

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -285,17 +285,37 @@ Control execution guarantees:
285285
```typescript
286286
import { StepSemantics } from "@aws/durable-execution-sdk-js";
287287

288-
// At-most-once per retry (default)
288+
// At-least-once per retry (default)
289+
await context.step("retriable-operation", async () => sendNotification(), {
290+
semantics: StepSemantics.AtLeastOncePerRetry,
291+
});
292+
293+
// At-most-once per retry
289294
await context.step("idempotent-operation", async () => updateDatabase(), {
290295
semantics: StepSemantics.AtMostOncePerRetry,
291296
});
297+
```
292298

293-
// At-least-once per retry
294-
await context.step("retriable-operation", async () => sendNotification(), {
295-
semantics: StepSemantics.AtLeastOncePerRetry,
296-
});
299+
**Important**: These semantics apply _per retry_, not per overall execution:
300+
301+
- **AtLeastOncePerRetry**: The step will execute at least once on each retry attempt. If the step succeeds but the checkpoint fails (e.g., sandbox crash), the step will re-execute on replay.
302+
- **AtMostOncePerRetry**: The step will execute at most once per retry attempt. A checkpoint is created before execution, so if a failure occurs after the checkpoint but before step completion, the previous step retry attempt is skipped on replay.
303+
304+
**To achieve at-most-once semantics on a step-level**, use a custom retry strategy:
305+
306+
```typescript
307+
await context.step(
308+
"truly-once-only",
309+
async () => callThatCannotTolerateDuplicates(),
310+
{
311+
semantics: StepSemantics.AtMostOncePerRetry,
312+
retryStrategy: () => ({ shouldRetry: false }), // No retries
313+
},
314+
);
297315
```
298316

317+
Without this, a step using `AtMostOncePerRetry` with retries enabled could still execute multiple times across different retry attempts.
318+
299319
### Jitter Strategies
300320

301321
Prevent thundering herd:

packages/aws-durable-execution-sdk-js/src/documents/CONCEPTS.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,22 @@ await context.step("api-call", async () => callExternalAPI(), {
316316
});
317317
```
318318

319+
### Step Semantics
320+
321+
Step semantics control execution guarantees _per retry attempt_:
322+
323+
- **AtLeastOncePerRetry** (default): The step executes at least once per retry. If the step succeeds but checkpointing fails, it re-executes on replay.
324+
- **AtMostOncePerRetry**: A checkpoint is created before execution. If failure occurs after checkpoint but before completion, the previous step retry attempt is skipped on replay.
325+
326+
**Important**: These guarantees are per retry, not per workflow execution. With retries enabled, `AtMostOncePerRetry` could still result in multiple executions across retry attempts. For step-level at-most-once semantics, use a custom retry policy:
327+
328+
```typescript
329+
await context.step("critical-operation", async () => attemptOnlyOnce(), {
330+
semantics: StepSemantics.AtMostOncePerRetry,
331+
retryStrategy: () => ({ shouldRetry: false }), // Disable retries
332+
});
333+
```
334+
319335
## Limitations and Considerations
320336

321337
- Code outside steps must be deterministic

packages/aws-durable-execution-sdk-js/src/types/step.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,56 @@ export interface RetryDecision {
3636
}
3737

3838
/**
39+
* Execution semantics for step operations.
40+
*
41+
* @remarks
42+
* These semantics control how step execution is checkpointed and replayed. **Important**: The guarantees apply *per
43+
* retry attempt*, not per overall workflow execution.
44+
*
45+
* With retries enabled (the default), a step could execute multiple times across different retry attempts even when
46+
* using `AtMostOncePerRetry`. To achieve step-level at-most-once execution, combine `AtMostOncePerRetry` with a retry
47+
* strategy that disables retries (`shouldRetry: false`).
48+
*
49+
* @example
50+
* ```typescript
51+
* // At-least-once per retry (default) - safe for idempotent operations
52+
* await context.step("send-notification", async () => sendEmail(), {
53+
* semantics: StepSemantics.AtLeastOncePerRetry,
54+
* });
55+
*
56+
* // At-most-once per retry - for non-idempotent operations
57+
* await context.step("charge-payment", async () => processPayment(), {
58+
* semantics: StepSemantics.AtMostOncePerRetry,
59+
* retryStrategy: () => ({ shouldRetry: false }),
60+
* });
61+
* ```
62+
*
3963
* @public
4064
*/
4165
export enum StepSemantics {
66+
/**
67+
* At-most-once execution per retry attempt.
68+
*
69+
* @remarks
70+
* A checkpoint is created before step execution. If a failure occurs after the checkpoint
71+
* but before step completion, the previous step retry attempt is skipped on replay.
72+
*
73+
* **Note**: This is "at-most-once *per retry*". With multiple retry attempts, the step
74+
* could still execute multiple times across different retries. To guarantee the step
75+
* executes at most once, disable retries by returning
76+
* `{ shouldRetry: false }` from your retry strategy.
77+
*/
4278
AtMostOncePerRetry = "AT_MOST_ONCE_PER_RETRY",
79+
80+
/**
81+
* At-least-once execution per retry attempt (default).
82+
*
83+
* @remarks
84+
* The step will execute at least once on each retry attempt. If the step succeeds
85+
* but the checkpoint fails (e.g., due to a sandbox crash), the step will re-execute
86+
* on replay. This is the safer default for operations that are idempotent or can
87+
* tolerate duplicate execution.
88+
*/
4389
AtLeastOncePerRetry = "AT_LEAST_ONCE_PER_RETRY",
4490
}
4591

0 commit comments

Comments
 (0)