You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Control how steps behave when interrupted mid-execution:
148
148
@@ -152,49 +152,79 @@ Control how steps behave when interrupted mid-execution:
152
152
|`AT_MOST_ONCE_PER_RETRY`| Never re-executes; throws `StepInterruptedException` if interrupted | Non-idempotent operations (sending emails, charging payments) |
153
153
154
154
```java
155
-
// Default: at-least-once (step may re-run if interrupted)
155
+
// Default: at-least-once per retry (step may re-run if interrupted)
156
156
var result = ctx.step("idempotent-update", Result.class,
157
157
() -> database.upsert(record));
158
158
159
-
// At-most-once: step will not re-run if interrupted
159
+
// At-most-once per retry
160
160
var result = ctx.step("send-email", Result.class,
161
161
() -> emailService.send(notification),
162
162
StepConfig.builder()
163
163
.semantics(StepSemantics.AT_MOST_ONCE_PER_RETRY)
164
164
.build());
165
165
```
166
166
167
-
With `AT_MOST_ONCE_PER_RETRY`, if a step starts but the function is interrupted before the result is checkpointed, the SDK throws `StepInterruptedException` on replay instead of re-executing. Handle this to implement your own recovery logic.
167
+
**Important**: These semantics apply *per retry attempt*, not per overall execution:
168
+
169
+
-**AT_LEAST_ONCE_PER_RETRY**: The step executes at least once per retry. If the step succeeds but checkpointing fails (e.g., sandbox crash), the step re-executes on replay.
170
+
-**AT_MOST_ONCE_PER_RETRY**: A checkpoint is created before execution. If failure occurs after checkpoint but before completion, the step is skipped on replay and `StepInterruptedException` is thrown.
171
+
172
+
To achieve step-level at-most-once semantics, combine with a no-retry strategy:
173
+
174
+
```java
175
+
// True at-most-once: step executes at most once, ever
176
+
var result = ctx.step("charge-payment", Result.class,
177
+
() -> paymentService.charge(amount),
178
+
StepConfig.builder()
179
+
.semantics(StepSemantics.AT_MOST_ONCE_PER_RETRY)
180
+
.retryStrategy(RetryStrategies.Presets.NO_RETRY)
181
+
.build());
182
+
```
183
+
184
+
Without this, a step using `AT_MOST_ONCE_PER_RETRY` with retries enabled could still execute multiple times across different retry attempts.
168
185
169
186
### Generic Types
170
187
171
-
For complex generic types like `List<User>`, use `TypeToken`:
188
+
When a step returns a parameterized type like `List<User>`, use `TypeToken` to preserve the type information:
172
189
173
190
```java
174
191
var users = ctx.step("fetch-users", newTypeToken<List<User>>() {},
175
192
() -> userService.getAllUsers());
193
+
194
+
var orderMap = ctx.step("fetch-orders", newTypeToken<Map<String, Order>>() {},
195
+
() -> orderService.getOrdersByCustomer());
176
196
```
177
197
198
+
This is needed for the SDK to deserialize a checkpointed result and get the exact type to reconstruct. See [TypeToken and Type Erasure](docs/internal-design.md#typetoken-and-type-erasure) for technical details.
199
+
178
200
## Configuration
179
201
180
202
Customize SDK behavior by overriding `createConfiguration()` in your handler:
@@ -74,7 +74,7 @@ public class MyHandler extends DurableHandler<Input, Output> {
74
74
|`serDes`|`JacksonSerDes`|
75
75
|`executor`|`Executors.newCachedThreadPool()`|
76
76
77
-
### Per-Step Configuration
77
+
### Step Configuration
78
78
79
79
```java
80
80
context.step("name", Type.class, supplier,
@@ -94,6 +94,16 @@ public interface SerDes {
94
94
}
95
95
```
96
96
97
+
**TypeToken and Type Erasure:**
98
+
99
+
Java's type erasure removes generic type parameters at runtime (`List<User>` becomes `List`). This is problematic for deserialization—Jackson needs the full type to reconstruct objects correctly.
100
+
101
+
`TypeToken<T>` solves this by capturing generic types at compile time. Creating `new TypeToken<List<User>>() {}` produces an anonymous subclass whose superclass type parameter is preserved in bytecode and accessible via reflection (`getGenericSuperclass()`).
102
+
103
+
The `SerDes` interface provides both `Class<T>` and `TypeToken<T>` overloads:
104
+
- Use `Class<T>` for simple types: `String.class`, `User.class`
105
+
- Use `TypeToken<T>` for parameterized types: `new TypeToken<List<User>>() {}`
106
+
97
107
**Custom RetryStrategy Interface:**
98
108
```java
99
109
@FunctionalInterface
@@ -107,24 +117,24 @@ public interface RetryStrategy {
0 commit comments