Skip to content

Commit 27c73cf

Browse files
committed
docs: linear retry strategy and retry helpers
Document the linear retry strategy and the per-language retry helpers in docs/sdk-reference/error-handling/retries.md. Closes #155 and #209. - Restructure 'Configure a retry strategy' into sibling subsections for 'Exponential backoff' and 'Linear backoff', each with a walkthrough, config signature, and delay-calculation block. - Add a 'Retry any durable operation' section for the helper (TypeScript withRetry, Python with_retry, Java withRetry and withRetryAsync) and contrast it with a step's own retryStrategy. - Revise 'Retry presets' to document the new linear preset across all three languages and Python's RetryPresets.fixed(interval). - Add nine new example files (linear-retry-strategy, linear-retry-strategy-config-signature, with-retry-helper) across TypeScript, Python, and Java. - Refresh the existing retry-presets examples in all three languages to include the new presets. All signatures verified against the SDK source on origin/main: TypeScript createLinearRetryStrategy and withRetry, Python create_linear_retry_strategy and with_retry, Java RetryStrategies.linearBackoff overloads and DurableContext.withRetry. Closes #155 Closes #209
1 parent de63adf commit 27c73cf

13 files changed

Lines changed: 420 additions & 12 deletions

File tree

docs/sdk-reference/error-handling/retries.md

Lines changed: 166 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ with up to 5 retries (6 total attempts). See [Retry presets](#retry-presets).
1818

1919
A retry strategy is a function that takes the error and the current attempt number, and
2020
returns a decision. The decision is either to retry with a given delay, or to stop. You
21-
can write a retry strategy directly yourself or use the built-in helper to build a
22-
ready-made retry strategy from configuration.
21+
can write a retry strategy directly yourself or use one of the built-in helpers to build
22+
a ready-made strategy from configuration. The SDK ships helpers for exponential backoff
23+
and linear backoff.
2324

24-
### RetryStrategy helper
25+
### Exponential backoff
2526

2627
=== "TypeScript"
2728

@@ -145,6 +146,110 @@ final_delay = jitter(base_delay), minimum 1 second
145146
- `JitterStrategy.HALF` randomizes between 50% and 100% of `base_delay`.
146147
- `JitterStrategy.NONE` uses the exact calculated delay.
147148

149+
### Linear backoff
150+
151+
Linear backoff grows the delay by a fixed `increment` on each attempt instead of
152+
multiplying by a backoff rate. Use it when you want predictable, bounded growth between
153+
retries rather than the rapid expansion of exponential backoff.
154+
155+
=== "TypeScript"
156+
157+
Use `createLinearRetryStrategy()` to build a strategy, then pass it as
158+
`retryStrategy` in `StepConfig`.
159+
160+
```typescript
161+
--8<-- "examples/typescript/sdk-reference/error-handling/linear-retry-strategy.ts"
162+
```
163+
164+
=== "Python"
165+
166+
Use `create_linear_retry_strategy()` with a `LinearRetryStrategyConfig`, then pass it
167+
as `retry_strategy` in `StepConfig`.
168+
169+
```python
170+
--8<-- "examples/python/sdk-reference/error-handling/linear-retry-strategy.py"
171+
```
172+
173+
=== "Java"
174+
175+
Use `RetryStrategies.linearBackoff()` to build a strategy, then pass it to
176+
`StepConfig.builder().retryStrategy()`.
177+
178+
```java
179+
--8<-- "examples/java/sdk-reference/error-handling/linear-retry-strategy.java"
180+
```
181+
182+
#### LinearRetryStrategyConfig signature
183+
184+
=== "TypeScript"
185+
186+
```typescript
187+
--8<-- "examples/typescript/sdk-reference/error-handling/linear-retry-strategy-config-signature.ts"
188+
```
189+
190+
**Parameters:**
191+
192+
- `maxAttempts` (optional) Total attempts including the initial attempt. Default: `6`.
193+
- `initialDelay` (optional) Delay before the first retry. Default: `{ seconds: 1 }`.
194+
- `increment` (optional) Amount added to the delay on each retry. Default:
195+
`{ seconds: 1 }`.
196+
- `maxDelay` (optional) Maximum delay between retries. Default: `{ minutes: 5 }`.
197+
- `jitter` (optional) A `JitterStrategy` value. Default: `JitterStrategy.FULL`.
198+
- `retryableErrors` (optional) Array of strings or `RegExp` patterns matched against
199+
the error message. The SDK retries all errors when you set neither
200+
`retryableErrors` nor `retryableErrorTypes`.
201+
- `retryableErrorTypes` (optional) Array of error classes. The SDK retries only
202+
errors that are instances of these classes. When you set both filters, the SDK
203+
retries an error if it matches either (OR logic).
204+
205+
=== "Python"
206+
207+
```python
208+
--8<-- "examples/python/sdk-reference/error-handling/linear-retry-strategy-config-signature.py"
209+
```
210+
211+
**Parameters:**
212+
213+
- `max_attempts` (optional) Total attempts including the initial attempt. Default: `6`.
214+
- `initial_delay` (optional) A `Duration`. Default: `Duration.from_seconds(1)`.
215+
- `increment` (optional) Amount added to the delay on each retry. Default:
216+
`Duration.from_seconds(1)`.
217+
- `max_delay` (optional) A `Duration`. Default: `Duration.from_minutes(5)`.
218+
- `jitter_strategy` (optional) A `JitterStrategy` value. Default:
219+
`JitterStrategy.FULL`.
220+
- `retryable_errors` (optional) List of strings or compiled `re.Pattern` objects
221+
matched against the error message. The SDK retries all errors when you set
222+
neither `retryable_errors` nor `retryable_error_types`.
223+
- `retryable_error_types` (optional) List of exception classes. The SDK retries only
224+
exceptions that are instances of these classes. When you set both filters, the
225+
SDK retries an error if it matches either (OR logic).
226+
227+
=== "Java"
228+
229+
```java
230+
--8<-- "examples/java/sdk-reference/error-handling/linear-retry-strategy-config-signature.java"
231+
```
232+
233+
**Parameters:**
234+
235+
- `maxAttempts` Total attempts including the initial attempt.
236+
- `initialDelay` A `java.time.Duration`. Minimum 1 second.
237+
- `maxDelay` A `java.time.Duration`. Minimum 1 second. Caps the calculated delay.
238+
- `increment` A `java.time.Duration` added to the delay on each retry.
239+
- `jitter` A `JitterStrategy` value. The three-argument overload omits both
240+
`maxDelay` and `jitter`.
241+
242+
#### Delay calculation
243+
244+
Linear backoff calculates the delay before each retry as:
245+
246+
```
247+
base_delay = min(initial_delay + increment × (attempt - 1), max_delay)
248+
final_delay = jitter(base_delay), minimum 1 second
249+
```
250+
251+
The same `JitterStrategy` values apply: `FULL`, `HALF`, and `NONE`.
252+
148253
### Write a custom strategy
149254

150255
You can write your own retry strategy directly. The SDK calls it with the error and the
@@ -210,6 +315,9 @@ The SDK ships with preset strategies for common cases:
210315
**`retryPresets.default`** 6 attempts, 5s initial delay, 60s max, 2x backoff, full
211316
jitter.
212317

318+
**`retryPresets.linear`** 6 attempts with linear delays of 1s, 2s, 3s, 4s, 5s and no
319+
jitter.
320+
213321
**`retryPresets.noRetry`** 1 attempt, fails immediately on error.
214322

215323
=== "Python"
@@ -231,6 +339,12 @@ The SDK ships with preset strategies for common cases:
231339
**`RetryPresets.critical()`** 10 attempts, 1s initial delay, 60s max, 1.5x backoff, no
232340
jitter.
233341

342+
**`RetryPresets.linear()`** 6 attempts with linear delays of 1s, 2s, 3s, 4s, 5s and no
343+
jitter.
344+
345+
**`RetryPresets.fixed(interval)`** 5 attempts at a constant interval. Defaults to a
346+
5 second interval. Pass a `Duration` to override.
347+
234348
=== "Java"
235349

236350
```java
@@ -240,8 +354,57 @@ The SDK ships with preset strategies for common cases:
240354
**`RetryStrategies.Presets.DEFAULT`** 6 attempts, 5s initial delay, 60s max, 2x backoff,
241355
full jitter.
242356

357+
**`RetryStrategies.Presets.LINEAR`** 6 attempts with linear delays capped at 5 seconds
358+
and no jitter.
359+
243360
**`RetryStrategies.Presets.NO_RETRY`** Fails immediately on first error.
244361

362+
## Retry any durable operation
363+
364+
Use the `withRetry` helper to wrap any durable operation in a replay-safe retry loop.
365+
The `withRetry` helper extends the same `RetryStrategy` configuration capability
366+
available to `step` to other operations, such as `invoke`, `waitForCallback`, and
367+
`waitForCondition`.
368+
369+
=== "TypeScript"
370+
371+
`withRetry(context, name?, func, config)` runs `func` and retries it on failure.
372+
The function receives the durable context and the 1-based attempt number. By
373+
default the loop is wrapped in `runInChildContext` so all attempts group under one
374+
operation in execution history.
375+
376+
```typescript
377+
--8<-- "examples/typescript/sdk-reference/error-handling/with-retry-helper.ts"
378+
```
379+
380+
=== "Python"
381+
382+
`with_retry(context, func, config, name=None)` runs `func` and retries it on
383+
failure. The function receives the durable context and the 1-based attempt number.
384+
By default the loop is wrapped in `run_in_child_context` so all attempts group
385+
under one operation in execution history.
386+
387+
```python
388+
--8<-- "examples/python/sdk-reference/error-handling/with-retry-helper.py"
389+
```
390+
391+
=== "Java"
392+
393+
`DurableContext.withRetry(name, operation, config)` runs `operation` and retries
394+
it on failure. The `BiFunction` receives the 1-based attempt number first and the
395+
durable context second. An async overload, `withRetryAsync`, returns a
396+
`DurableFuture<T>` for parallel use.
397+
398+
```java
399+
--8<-- "examples/java/sdk-reference/error-handling/with-retry-helper.java"
400+
```
401+
402+
The `withRetry` helper wraps the retry loop in a child context and uses `context.wait`
403+
between attempts to suspend the invocation while waiting for the retry interval. The
404+
child context, the wait operations, and any operations inside each attempt count toward
405+
the durable operations the execution consumes. See
406+
[AWS Lambda service quotas](https://docs.aws.amazon.com/general/latest/gr/lambda-service.html).
407+
245408
## Retry only specific errors
246409

247410
You can retry only certain error types and fail immediately on others.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
RetryStrategy RetryStrategies.linearBackoff(
2+
int maxAttempts,
3+
Duration initialDelay,
4+
Duration increment
5+
)
6+
7+
RetryStrategy RetryStrategies.linearBackoff(
8+
int maxAttempts,
9+
Duration initialDelay,
10+
Duration maxDelay,
11+
Duration increment,
12+
JitterStrategy jitter
13+
)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import java.time.Duration;
2+
import software.amazon.lambda.durable.DurableContext;
3+
import software.amazon.lambda.durable.DurableHandler;
4+
import software.amazon.lambda.durable.StepContext;
5+
import software.amazon.lambda.durable.config.StepConfig;
6+
import software.amazon.lambda.durable.retry.JitterStrategy;
7+
import software.amazon.lambda.durable.retry.RetryStrategies;
8+
9+
public class LinearRetryStrategyExample extends DurableHandler<Object, String> {
10+
@Override
11+
public String handleRequest(Object input, DurableContext context) {
12+
StepConfig config = StepConfig.builder()
13+
.retryStrategy(RetryStrategies.linearBackoff(
14+
5, // maxAttempts
15+
Duration.ofSeconds(2), // initialDelay
16+
Duration.ofSeconds(30), // maxDelay
17+
Duration.ofSeconds(3), // increment
18+
JitterStrategy.FULL)) // jitter
19+
.build();
20+
21+
return context.step("call-external-api", String.class,
22+
(StepContext ctx) -> "ok",
23+
config);
24+
}
25+
}

examples/java/sdk-reference/error-handling/retry-presets.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,20 @@ public Map<String, Object> handleRequest(Map<String, Object> event, DurableConte
1313
stepCtx -> callApi(),
1414
StepConfig.builder().retryStrategy(RetryStrategies.Presets.DEFAULT).build());
1515

16+
// Linear: 6 attempts, delays of 1s, 2s, 3s, 4s, 5s
17+
String audit = context.step("audit-log", String.class,
18+
stepCtx -> writeAuditLog(),
19+
StepConfig.builder().retryStrategy(RetryStrategies.Presets.LINEAR).build());
20+
1621
// No retry: fail immediately on first error
1722
String critical = context.step("charge-payment", String.class,
1823
stepCtx -> chargePayment(),
1924
StepConfig.builder().retryStrategy(RetryStrategies.Presets.NO_RETRY).build());
2025

21-
return Map.of("result", result, "critical", critical);
26+
return Map.of("result", result, "audit", audit, "critical", critical);
2227
}
2328

2429
private String callApi() { return "ok"; }
30+
private String writeAuditLog() { return "logged"; }
2531
private String chargePayment() { return "charged"; }
2632
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import java.time.Duration;
2+
import java.util.Map;
3+
import software.amazon.lambda.durable.DurableContext;
4+
import software.amazon.lambda.durable.DurableHandler;
5+
import software.amazon.lambda.durable.config.WithRetryConfig;
6+
import software.amazon.lambda.durable.retry.JitterStrategy;
7+
import software.amazon.lambda.durable.retry.RetryStrategies;
8+
9+
public class WithRetryHelperExample extends DurableHandler<Map<String, Object>, String> {
10+
11+
@Override
12+
public String handleRequest(Map<String, Object> event, DurableContext context) {
13+
WithRetryConfig retryConfig = WithRetryConfig.builder()
14+
.retryStrategy(RetryStrategies.exponentialBackoff(
15+
3,
16+
Duration.ofSeconds(2),
17+
Duration.ofMinutes(1),
18+
2.0,
19+
JitterStrategy.FULL))
20+
.build();
21+
22+
// invoke does not accept a retry strategy, so withRetry applies backoff
23+
// between failed attempts.
24+
return context.withRetry(
25+
"charge-payment",
26+
(attempt, ctx) -> ctx.invoke(
27+
"charge-" + attempt,
28+
"process-payment",
29+
Map.of("orderId", event.get("orderId")),
30+
String.class),
31+
retryConfig);
32+
}
33+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import re
2+
from dataclasses import dataclass
3+
from aws_durable_execution_sdk_python.config import Duration, JitterStrategy
4+
5+
6+
@dataclass
7+
class LinearRetryStrategyConfig:
8+
max_attempts: int = 6
9+
initial_delay: Duration = Duration.from_seconds(1)
10+
increment: Duration = Duration.from_seconds(1)
11+
max_delay: Duration = Duration.from_minutes(5)
12+
jitter_strategy: JitterStrategy = JitterStrategy.FULL
13+
retryable_errors: list[str | re.Pattern] | None = None
14+
retryable_error_types: list[type[Exception]] | None = None
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from aws_durable_execution_sdk_python.config import Duration, StepConfig
2+
from aws_durable_execution_sdk_python.context import DurableContext, StepContext, durable_step
3+
from aws_durable_execution_sdk_python.execution import durable_execution
4+
from aws_durable_execution_sdk_python.retries import (
5+
LinearRetryStrategyConfig,
6+
create_linear_retry_strategy,
7+
)
8+
9+
retry_strategy = create_linear_retry_strategy(
10+
LinearRetryStrategyConfig(
11+
max_attempts=5,
12+
initial_delay=Duration.from_seconds(2),
13+
increment=Duration.from_seconds(3),
14+
max_delay=Duration.from_seconds(30),
15+
)
16+
)
17+
18+
step_config = StepConfig(retry_strategy=retry_strategy)
19+
20+
21+
@durable_step
22+
def call_external_api(step_context: StepContext) -> str:
23+
return "ok"
24+
25+
26+
@durable_execution
27+
def lambda_handler(event: dict, context: DurableContext) -> str:
28+
return context.step(call_external_api(), config=step_config)
Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
1+
from aws_durable_execution_sdk_python.config import Duration, StepConfig
12
from aws_durable_execution_sdk_python.retries import RetryPresets
2-
from aws_durable_execution_sdk_python.config import StepConfig
33

44
# No retries
5-
step_config = StepConfig(retry_strategy=RetryPresets.none())
5+
no_retry_config = StepConfig(retry_strategy=RetryPresets.none())
66

7-
# Default retries (6 attempts, 5s initial delay)
8-
step_config = StepConfig(retry_strategy=RetryPresets.default())
7+
# Default retries (6 attempts, 5s initial delay, 60s max, 2x backoff)
8+
default_config = StepConfig(retry_strategy=RetryPresets.default())
99

1010
# Quick retries for transient errors (3 attempts)
11-
step_config = StepConfig(retry_strategy=RetryPresets.transient())
11+
transient_config = StepConfig(retry_strategy=RetryPresets.transient())
1212

1313
# Longer retries for resource availability (5 attempts, up to 5 minutes)
14-
step_config = StepConfig(retry_strategy=RetryPresets.resource_availability())
14+
resource_config = StepConfig(retry_strategy=RetryPresets.resource_availability())
1515

1616
# Aggressive retries for critical operations (10 attempts)
17-
step_config = StepConfig(retry_strategy=RetryPresets.critical())
17+
critical_config = StepConfig(retry_strategy=RetryPresets.critical())
18+
19+
# Linear backoff (6 attempts, delays of 1s, 2s, 3s, 4s, 5s)
20+
linear_config = StepConfig(retry_strategy=RetryPresets.linear())
21+
22+
# Fixed delay (5 attempts, 5 second interval). Pass an interval to customize.
23+
fixed_config = StepConfig(retry_strategy=RetryPresets.fixed())
24+
fixed_2s_config = StepConfig(
25+
retry_strategy=RetryPresets.fixed(interval=Duration.from_seconds(2))
26+
)

0 commit comments

Comments
 (0)