Skip to content

Commit 9532ae8

Browse files
committed
fix: adapted readme
1 parent 4c46706 commit 9532ae8

3 files changed

Lines changed: 81 additions & 41 deletions

File tree

README.md

Lines changed: 56 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public class OrderProcessor extends DurableHandler<Order, OrderResult> {
6464

6565
### step() – Execute with Checkpointing
6666

67-
Steps execute your code and checkpoint the result. On replay, cached results are returned without re-execution.
67+
Steps execute your code and checkpoint the result. On replay, results from completed checkpoints are returned without re-execution.
6868

6969
```java
7070
// Basic step (blocks until complete)
@@ -83,7 +83,7 @@ See [Step Configuration](#step-configuration) for retry strategies, delivery sem
8383

8484
### stepAsync() and DurableFuture – Concurrent Operations
8585

86-
`stepAsync()` starts a step in the background and returns a `DurableFuture<T>` immediately. This enables concurrent execution patterns.
86+
`stepAsync()` starts a step in the background and returns a `DurableFuture<T>`. This enables concurrent execution patterns.
8787

8888
```java
8989
// Start multiple operations concurrently
@@ -92,7 +92,7 @@ DurableFuture<User> userFuture = ctx.stepAsync("fetch-user", User.class,
9292
DurableFuture<List<Order>> ordersFuture = ctx.stepAsync("fetch-orders",
9393
new TypeToken<List<Order>>() {}, () -> orderService.getOrders(userId));
9494

95-
// Both operations run in parallel
95+
// Both operations run concurrently
9696
// Block and get results when needed
9797
User user = userFuture.get();
9898
List<Order> orders = ordersFuture.get();
@@ -129,10 +129,10 @@ Configure how steps handle transient failures:
129129

130130
```java
131131
// No retry - fail immediately (default)
132-
StepConfig.builder().retryStrategy(RetryStrategies.Presets.NO_RETRY).build()
132+
var noRetries = StepConfig.builder().retryStrategy(RetryStrategies.Presets.NO_RETRY).build()
133133

134134
// Exponential backoff with jitter
135-
StepConfig.builder()
135+
var customRetries = StepConfig.builder()
136136
.retryStrategy(RetryStrategies.exponentialBackoff(
137137
5, // max attempts
138138
Duration.ofSeconds(2), // initial delay
@@ -142,7 +142,7 @@ StepConfig.builder()
142142
.build()
143143
```
144144

145-
### Step Semantics
145+
### Step-Retry Semantics
146146

147147
Control how steps behave when interrupted mid-execution:
148148

@@ -152,49 +152,79 @@ Control how steps behave when interrupted mid-execution:
152152
| `AT_MOST_ONCE_PER_RETRY` | Never re-executes; throws `StepInterruptedException` if interrupted | Non-idempotent operations (sending emails, charging payments) |
153153

154154
```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)
156156
var result = ctx.step("idempotent-update", Result.class,
157157
() -> database.upsert(record));
158158

159-
// At-most-once: step will not re-run if interrupted
159+
// At-most-once per retry
160160
var result = ctx.step("send-email", Result.class,
161161
() -> emailService.send(notification),
162162
StepConfig.builder()
163163
.semantics(StepSemantics.AT_MOST_ONCE_PER_RETRY)
164164
.build());
165165
```
166166

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.
168185

169186
### Generic Types
170187

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:
172189

173190
```java
174191
var users = ctx.step("fetch-users", new TypeToken<List<User>>() {},
175192
() -> userService.getAllUsers());
193+
194+
var orderMap = ctx.step("fetch-orders", new TypeToken<Map<String, Order>>() {},
195+
() -> orderService.getOrdersByCustomer());
176196
```
177197

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+
178200
## Configuration
179201

180202
Customize SDK behavior by overriding `createConfiguration()` in your handler:
181203

182204
```java
183-
@Override
184-
protected DurableConfig createConfiguration() {
185-
// Custom Lambda client with connection pooling
186-
var lambdaClient = LambdaClient.builder()
187-
.httpClient(ApacheHttpClient.builder()
188-
.maxConnections(50)
189-
.connectionTimeout(Duration.ofSeconds(30))
190-
.build())
191-
.build();
192-
193-
return DurableConfig.builder()
194-
.withLambdaClient(lambdaClient)
195-
.withSerDes(new MyCustomSerDes()) // Custom serialization
196-
.withExecutorService(Executors.newFixedThreadPool(10)) // Custom thread pool
197-
.build();
205+
public class OrderProcessor extends DurableHandler<Order, OrderResult> {
206+
207+
@Override
208+
protected DurableConfig createConfiguration() {
209+
// Custom Lambda client with connection pooling
210+
var lambdaClient = LambdaClient.builder()
211+
.httpClient(ApacheHttpClient.builder()
212+
.maxConnections(50)
213+
.connectionTimeout(Duration.ofSeconds(30))
214+
.build())
215+
.build();
216+
217+
return DurableConfig.builder()
218+
.withLambdaClient(lambdaClient)
219+
.withSerDes(new MyCustomSerDes()) // Custom serialization
220+
.withExecutorService(Executors.newFixedThreadPool(10)) // Custom thread pool
221+
.build();
222+
}
223+
224+
@Override
225+
protected OrderResult handleRequest(Order order, DurableContext ctx) {
226+
// Your handler logic
227+
}
198228
}
199229
```
200230

@@ -311,7 +341,7 @@ Test against deployed Lambda functions:
311341

312342
```java
313343
var runner = CloudDurableTestRunner.create(
314-
"arn:aws:lambda:us-east-1:123456789012:function:order-processor",
344+
"arn:aws:lambda:us-east-1:123456789012:function:order-processor:$LATEST",
315345
Order.class,
316346
OrderResult.class);
317347

docs/internal-design.md

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Context getLambdaContext()
5353
T get() // Blocks until complete, may suspend
5454
```
5555

56-
### Configuration
56+
### Handler Configuration
5757

5858
```java
5959
public class MyHandler extends DurableHandler<Input, Output> {
@@ -74,7 +74,7 @@ public class MyHandler extends DurableHandler<Input, Output> {
7474
| `serDes` | `JacksonSerDes` |
7575
| `executor` | `Executors.newCachedThreadPool()` |
7676

77-
### Per-Step Configuration
77+
### Step Configuration
7878

7979
```java
8080
context.step("name", Type.class, supplier,
@@ -94,6 +94,16 @@ public interface SerDes {
9494
}
9595
```
9696

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+
97107
**Custom RetryStrategy Interface:**
98108
```java
99109
@FunctionalInterface
@@ -107,24 +117,24 @@ public interface RetryStrategy {
107117

108118
```
109119
┌─────────────────────────────────────────────────────────────────────────┐
110-
│ Lambda Runtime
120+
│ Lambda Runtime │
111121
└─────────────────────────────────────────────────────────────────────────┘
112122
113123
114124
┌─────────────────────────────────────────────────────────────────────────┐
115-
│ DurableHandler<I,O>
116-
│ - Entry point (RequestStreamHandler)
117-
│ - Extracts input type via reflection
118-
│ - Delegates to DurableExecutor
125+
│ DurableHandler<I,O> │
126+
│ - Entry point (RequestStreamHandler) │
127+
│ - Extracts input type via reflection │
128+
│ - Delegates to DurableExecutor │
119129
└─────────────────────────────────────────────────────────────────────────┘
120130
121131
122132
┌─────────────────────────────────────────────────────────────────────────┐
123-
│ DurableExecutor
124-
│ - Creates ExecutionManager, DurableContext
125-
│ - Runs handler in executor
126-
│ - Waits for completion OR suspension
127-
│ - Returns SUCCESS/PENDING/FAILED
133+
│ DurableExecutor │
134+
│ - Creates ExecutionManager, DurableContext │
135+
│ - Runs handler in executor │
136+
│ - Waits for completion OR suspension │
137+
│ - Returns SUCCESS/PENDING/FAILED │
128138
└─────────────────────────────────────────────────────────────────────────┘
129139
130140
┌───────────────┴───────────────┐

examples/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ Example applications demonstrating the AWS Lambda Durable Execution SDK for Java
1212

1313
## Local Testing
1414

15-
Run examples locally without AWS using `LocalDurableTestRunner`:
15+
Run examples locally without deploying to AWS using `LocalDurableTestRunner`:
1616

1717
```bash
18-
# First, build and install the SDK to local Maven repo (from project root)
19-
mvn clean install -DskipTests
18+
# Build and install the SDK to local Maven repo (required since SDK is not yet published)
19+
mvn clean install -DskipTests # from project root
2020

2121
cd examples
2222

0 commit comments

Comments
 (0)