diff --git a/AGENTS.md b/AGENTS.md index a9d0fdf8c..c56211dce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -201,12 +201,13 @@ void testAgainstRealLambda() { | Class | Responsibility | |-------|----------------| | `DurableHandler` | Lambda entry point, extend this | -| `DurableContext` | User API: `step()`, `wait()`, `map()` | +| `DurableContext` | User API: `step()`, `wait()`, `map()`, `waitForCondition()` | | `DurableExecutor` | Orchestrates execution lifecycle | | `ExecutionManager` | Thread coordination, state management | | `CheckpointBatcher` | Batches checkpoint API calls (750KB limit) | | `StepOperation` | Executes steps with retry logic | | `WaitOperation` | Handles wait checkpointing | +| `WaitForConditionOperation` | Polls a condition function with configurable backoff | | `MapOperation` | Applies a function across items concurrently via child contexts | | `BaseConcurrentOperation` | Shared base for map/parallel: concurrency limiting, completion evaluation | diff --git a/README.md b/README.md index a6ddd7c62..6091df5f8 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Your durable function extends `DurableHandler` and implements `handleReque - `ctx.invoke()` – Invoke another Lambda function and wait for the result - `ctx.runInChildContext()` – Run an isolated child context with its own checkpoint log - `ctx.map()` – Apply a function to each item in a collection concurrently +- `ctx.waitForCondition()` – Poll a condition function until it signals done, suspending between polls ## Quick Start @@ -94,6 +95,7 @@ See [Deploy Lambda durable functions with Infrastructure as Code](https://docs.a - [Invoke](docs/core/invoke.md) - Call other durable functions - [Child Contexts](docs/core/child-contexts.md) - Organize complex workflows into isolated units - [Map](docs/core/map.md) - Apply a function across a collection concurrently +- [Wait for Condition](docs/core/wait-for-condition.md) - Poll a condition until it's met, with configurable backoff **Examples** diff --git a/docs/advanced/error-handling.md b/docs/advanced/error-handling.md index 5c8ef6b2e..7642d70c3 100644 --- a/docs/advanced/error-handling.md +++ b/docs/advanced/error-handling.md @@ -17,6 +17,7 @@ DurableExecutionException - General durable exception ├── CallbackException - General callback exception │ ├── CallbackFailedException - External system sent an error response to the callback. Handle the error or propagate failure │ └── CallbackTimeoutException - Callback exceeded its timeout duration. Handle the error or propagate the failure + ├── WaitForConditionException - waitForCondition exceeded max polling attempts or failed. Catch to implement fallback logic. └── ChildContextFailedException - Child context failed and the original exception could not be reconstructed ``` diff --git a/docs/core/wait-for-condition.md b/docs/core/wait-for-condition.md new file mode 100644 index 000000000..66750055d --- /dev/null +++ b/docs/core/wait-for-condition.md @@ -0,0 +1,130 @@ +## waitForCondition() – Poll Until a Condition is Met + +`waitForCondition` repeatedly calls a check function until it signals done. Between polls, the Lambda suspends without consuming compute. State is checkpointed after each check, so progress survives interruptions. + +```java +// Poll an order status until it ships +var status = ctx.waitForCondition( + "wait-for-shipment", + String.class, + (currentStatus, stepCtx) -> { + var latest = orderService.getStatus(orderId); + return "SHIPPED".equals(latest) + ? WaitForConditionResult.stopPolling(latest) + : WaitForConditionResult.continuePolling(latest); + }, + "PENDING"); +``` + +The check function receives the current state and a `StepContext`, and returns a `WaitForConditionResult`: +- `WaitForConditionResult.stopPolling(value)` — condition met, return `value` as the final result +- `WaitForConditionResult.continuePolling(value)` — keep polling, pass `value` to the next check + +The `initialState` parameter (`"PENDING"` above) is passed to the first check invocation. + +## waitForConditionAsync() – Non-Blocking Polling + +`waitForConditionAsync()` starts polling but returns a `DurableFuture` immediately, allowing other operations to run concurrently. + +```java +DurableFuture shipmentFuture = ctx.waitForConditionAsync( + "wait-for-shipment", + String.class, + (status, stepCtx) -> { + var latest = orderService.getStatus(orderId); + return "SHIPPED".equals(latest) + ? WaitForConditionResult.stopPolling(latest) + : WaitForConditionResult.continuePolling(latest); + }, + "PENDING"); + +// Do other work while polling runs +var invoice = ctx.step("generate-invoice", String.class, stepCtx -> generateInvoice(orderId)); + +// Block until the condition is met +var shipmentStatus = shipmentFuture.get(); +``` + +## Wait Strategies + +The wait strategy controls the delay between polls. By default, `waitForCondition` uses exponential backoff (60 max attempts, 5s initial delay, 300s max delay, 1.5x backoff rate, FULL jitter). + +Use `WaitStrategies` to configure a different strategy: + +```java +// Fixed 30-second delay, up to 10 attempts +var config = WaitForConditionConfig.builder() + .waitStrategy(WaitStrategies.fixedDelay(10, Duration.ofSeconds(30))) + .build(); + +var result = ctx.waitForCondition("poll-status", String.class, checkFunc, "PENDING", config); +``` + +```java +// Custom exponential backoff +var config = WaitForConditionConfig.builder() + .waitStrategy(WaitStrategies.exponentialBackoff( + 20, // max attempts + Duration.ofSeconds(2), // initial delay + Duration.ofSeconds(60), // max delay + 2.0, // backoff rate + JitterStrategy.HALF)) // jitter + .build(); +``` + +| Factory Method | Description | +|----------------|-------------| +| `WaitStrategies.defaultStrategy()` | Exponential backoff: 60 attempts, 5s initial, 300s max, 1.5x rate, FULL jitter | +| `WaitStrategies.exponentialBackoff(...)` | Custom exponential backoff with configurable parameters | +| `WaitStrategies.fixedDelay(maxAttempts, delay)` | Constant delay between polls | +| `WaitStrategies.Presets.DEFAULT` | Same as `defaultStrategy()`, as a static constant | + +## Configuration + +`WaitForConditionConfig` holds optional parameters. All fields have sensible defaults, so you only need it when customizing: + +```java +var config = WaitForConditionConfig.builder() + .waitStrategy(WaitStrategies.fixedDelay(10, Duration.ofSeconds(5))) + .serDes(new CustomSerDes()) + .build(); +``` + +| Option | Default | Description | +|--------|---------|-------------| +| `waitStrategy()` | Exponential backoff (see above) | Controls delay between polls and max attempts | +| `serDes()` | Handler default | Custom serialization for checkpointing state | + +## Error Handling + +| Exception | When Thrown | +|-----------|-------------| +| `WaitForConditionException` | Max attempts exceeded (thrown by the wait strategy) | +| `SerDesException` | Checkpointed state fails to deserialize on replay | +| User's exception | Check function throws — propagated through `get()` | + +```java +try { + var result = ctx.waitForCondition("poll", String.class, checkFunc, "initial"); +} catch (WaitForConditionException e) { + // Max attempts exceeded — condition was never met +} catch (IllegalStateException e) { + // Check function threw this — handle accordingly +} +``` + +## Custom Wait Strategies + +You can write a custom strategy by implementing `WaitForConditionWaitStrategy`: + +```java +WaitForConditionWaitStrategy customStrategy = (state, attempt) -> { + // Vary delay based on state + if ("ALMOST_READY".equals(state)) { + return Duration.ofSeconds(2); // Poll faster when close + } + return Duration.ofSeconds(30); // Otherwise poll slowly +}; +``` + +The strategy receives the current state and attempt number, and returns a `Duration`. Throw `WaitForConditionException` to stop polling with an error. diff --git a/docs/design.md b/docs/design.md index d9e445cba..97cb6015d 100644 --- a/docs/design.md +++ b/docs/design.md @@ -189,15 +189,16 @@ context.step("name", Type.class, supplier, │ - User-facing API │ │ - State (ops, token) │ │ - step(), stepAsync(), etc │ │ - Thread coordination │ │ - wait(), waitAsync() │ │ - Checkpoint batching │ -│ - Operation ID counter │ │ - Checkpoint response handling │ -└──────────────────────────────┘ │ - Polling │ - │ └─────────────────────────────────┘ +│ - waitForCondition() │ │ - Checkpoint response handling │ +│ - Operation ID counter │ │ - Polling │ +└──────────────────────────────┘ └─────────────────────────────────┘ │ │ ▼ ▼ ┌──────────────────────────────┐ ┌──────────────────────────────┐ │ Operations │ │ CheckpointBatcher │ │ - StepOperation │ │ - Queues requests │ │ - WaitOperation │ │ - Batches API calls (750KB) │ +│ - WaitForConditionOperation │ │ │ │ - execute() / get() │ │ - Notifies via callback │ └──────────────────────────────┘ └──────────────────────────────┘ │ @@ -233,7 +234,8 @@ software.amazon.lambda.durable │ ├── StepOperation # Step logic │ ├── InvokeOperation # Invoke logic │ ├── CallbackOperation # Callback logic -│ └── WaitOperation # Wait logic +│ ├── WaitOperation # Wait logic +│ └── WaitForConditionOperation # Polling condition logic │ ├── logging/ │ ├── DurableLogger # Context-aware logger wrapper (MDC-based) @@ -243,7 +245,9 @@ software.amazon.lambda.durable │ ├── RetryStrategy # Interface │ ├── RetryStrategies # Presets │ ├── RetryDecision # shouldRetry + delay -│ └── JitterStrategy # Jitter options +│ ├── JitterStrategy # Jitter options +│ ├── WaitForConditionWaitStrategy # Polling delay interface +│ └── WaitStrategies # Polling strategy factory + Presets │ ├── client/ │ ├── DurableExecutionClient # Interface @@ -264,6 +268,7 @@ software.amazon.lambda.durable ├── NonDeterministicExecutionException ├── StepFailedException ├── StepInterruptedException + ├── WaitForConditionException └── SerDesException ``` @@ -356,6 +361,7 @@ sequenceDiagram DurableExecutionException (base) ├── StepFailedException # Step failed after all retries ├── StepInterruptedException # Step interrupted (AT_MOST_ONCE) +├── WaitForConditionException # Polling exceeded max attempts ├── NonDeterministicExecutionException # Replay mismatch └── SerDesException # Serialization error @@ -366,6 +372,7 @@ SuspendExecutionException # Internal: triggers suspension (not user-facin |-----------|---------|----------| | `StepFailedException` | Step throws after exhausting retries | Catch in handler or let fail | | `StepInterruptedException` | AT_MOST_ONCE step interrupted mid-execution | Treat as failure | +| `WaitForConditionException` | waitForCondition exceeded max polling attempts | Catch in handler or let fail | | `NonDeterministicExecutionException` | Replay finds different operation than expected | Bug in handler (non-deterministic code) | | `SerDesException` | Jackson fails to serialize/deserialize | Fix data model or custom SerDes | diff --git a/docs/spec/waitForCondition.md b/docs/spec/waitForCondition.md index 89a3accb1..1f93fdd92 100644 --- a/docs/spec/waitForCondition.md +++ b/docs/spec/waitForCondition.md @@ -2,22 +2,19 @@ ## Overview -This design adds a `waitForCondition` operation to the Java Durable Execution SDK. The operation periodically checks a user-supplied condition function, using a configurable wait strategy to determine polling intervals and termination. It follows the same checkpoint-and-replay model as existing operations (`step`, `wait`, `invoke`) and mirrors the JavaScript SDK's `waitForCondition` implementation. +`waitForCondition` is a durable operation that repeatedly polls a user-supplied check function until it signals done. Between polls, the Lambda suspends without consuming compute. State is checkpointed after each check, so progress survives interruptions. It follows the same checkpoint-and-replay model as existing operations (`step`, `wait`, `invoke`) and mirrors the JavaScript SDK's `waitForCondition` implementation. ## Architecture ### How it works -`waitForCondition` is implemented as a specialized step operation that uses the RETRY checkpoint action for polling iterations: - -1. User calls `ctx.waitForCondition(name, resultType, checkFunc, config)` +1. User calls `ctx.waitForCondition(name, resultType, checkFunc, initialState)` (or with optional config) 2. A `WaitForConditionOperation` is created with a unique operation ID 3. On first execution: - Checkpoint START with subtype `WAIT_FOR_CONDITION` - Execute the check function with `initialState` and a `StepContext` - - Call the wait strategy with the new state and attempt number - - If `stopPolling()`: checkpoint SUCCEED with the final state, return it - - If `continuePolling(delay)`: checkpoint RETRY with the state and delay, poll for READY, then loop + - If check function returns `WaitForConditionResult.stopPolling(value)`: checkpoint SUCCEED, return value + - If check function returns `WaitForConditionResult.continuePolling(value)`: call wait strategy to compute delay, checkpoint RETRY with state and delay, poll for READY, then loop - If check function throws: checkpoint FAIL, propagate the error 4. On replay: - SUCCEEDED: return cached result (skip re-execution) @@ -25,256 +22,207 @@ This design adds a `waitForCondition` operation to the Java Durable Execution SD - PENDING: wait for READY transition, then resume polling - STARTED/READY: resume execution from current attempt and state -This matches the JS SDK's behavior where each polling iteration is a RETRY on the same STEP operation. - -### New Classes +### File Structure ``` sdk/src/main/java/software/amazon/lambda/durable/ -├── WaitForConditionConfig.java # Config builder (waitStrategy, initialState, serDes) -├── WaitForConditionWaitStrategy.java # Functional interface: (T state, int attempt) → WaitForConditionDecision -├── WaitForConditionDecision.java # Sealed result: continuePolling(Duration) | stopPolling() -├── WaitStrategies.java # Factory with builder for common patterns +├── WaitForConditionResult.java # Check function return type (value + isDone) +├── WaitForConditionConfig.java # Optional config (wait strategy, custom SerDes) +├── retry/ +│ ├── WaitForConditionWaitStrategy.java # Functional interface: (T state, int attempt) → Duration +│ └── WaitStrategies.java # Factory methods + Presets.DEFAULT ├── operation/ -│ └── WaitForConditionOperation.java # Operation implementation -├── model/ -│ └── OperationSubType.java # Add WAIT_FOR_CONDITION enum value +│ └── WaitForConditionOperation.java # Operation implementation +├── exception/ +│ └── WaitForConditionException.java # Thrown when max attempts exceeded +└── model/ + └── OperationSubType.java # WAIT_FOR_CONDITION enum value ``` ### Class Diagram ``` -DurableContext - ├── waitForCondition(name, Class, checkFunc, config) → T - ├── waitForCondition(name, TypeToken, checkFunc, config) → T - ├── waitForConditionAsync(name, Class, checkFunc, config) → DurableFuture - └── waitForConditionAsync(name, TypeToken, checkFunc, config) → DurableFuture +DurableContext (interface) + ├── waitForCondition(name, Class, checkFunc, initialState) → T + ├── waitForCondition(name, Class, checkFunc, initialState, config) → T + ├── waitForCondition(name, TypeToken, checkFunc, initialState) → T + ├── waitForCondition(name, TypeToken, checkFunc, initialState, config) → T + ├── waitForConditionAsync(name, Class, checkFunc, initialState) → DurableFuture + ├── waitForConditionAsync(name, Class, checkFunc, initialState, config) → DurableFuture + ├── waitForConditionAsync(name, TypeToken, checkFunc, initialState) → DurableFuture + └── waitForConditionAsync(name, TypeToken, checkFunc, initialState, config) → DurableFuture │ ▼ WaitForConditionOperation extends BaseDurableOperation ├── start() → checkpoint START, execute check loop ├── replay(existing) → handle SUCCEEDED/FAILED/PENDING/STARTED/READY ├── get() → block, deserialize result or throw - └── executeCheckLoop(currentState, attempt) + └── executeCheckLogic(currentState, attempt) │ - ├── calls checkFunc(state, stepContext) → newState - ├── calls waitStrategy.evaluate(newState, attempt) → WaitForConditionDecision - │ ├── stopPolling() → checkpoint SUCCEED - │ └── continuePolling(delay) → checkpoint RETRY, poll, loop + ├── calls checkFunc(state, stepContext) → WaitForConditionResult + │ ├── stopPolling(value) → checkpoint SUCCEED + │ └── continuePolling(value) → call waitStrategy, checkpoint RETRY, poll, loop └── on error → checkpoint FAIL ``` ## Detailed Design -### WaitForConditionWaitStrategy (Functional Interface) +### WaitForConditionResult\ (Record) ```java -@FunctionalInterface -public interface WaitForConditionWaitStrategy { - WaitForConditionDecision evaluate(T state, int attempt); +public record WaitForConditionResult(T value, boolean isDone) { + public static WaitForConditionResult stopPolling(T value); + public static WaitForConditionResult continuePolling(T value); } ``` -- `state`: the current state returned by the check function -- `attempt`: 0-based attempt number (first check is attempt 0) -- Returns a `WaitForConditionDecision` indicating whether to continue or stop +Returned by the check function to signal whether the condition is met: +- `stopPolling(value)`: condition met, return `value` as the final result +- `continuePolling(value)`: keep polling, pass `value` to the next check and to the wait strategy -### WaitForConditionDecision +### WaitForConditionWaitStrategy\ (Functional Interface) ```java -public sealed interface WaitForConditionDecision { - record ContinuePolling(Duration delay) implements WaitForConditionDecision {} - record StopPolling() implements WaitForConditionDecision {} - - static WaitForConditionDecision continuePolling(Duration delay) { - return new ContinuePolling(delay); - } - - static WaitForConditionDecision stopPolling() { - return new StopPolling(); - } +@FunctionalInterface +public interface WaitForConditionWaitStrategy { + Duration evaluate(T state, int attempt); } ``` -Uses Java sealed interfaces for type safety. The `delay` in `ContinuePolling` must be >= 1 second (enforced at construction). +Computes the delay before the next poll. Only called when the check function returns `continuePolling`. Throws `WaitForConditionException` when max attempts exceeded. + +- `state`: the current state from the check function +- `attempt`: 0-based attempt number +- Returns: `Duration` delay before next poll + +Built-in strategies (from `WaitStrategies`) ignore the state parameter and compute delays based solely on the attempt number. ### WaitStrategies (Factory) ```java public final class WaitStrategies { - public static Builder builder(Predicate shouldContinuePolling) { ... } - public static class Builder { - // Defaults match JS SDK - private int maxAttempts = 60; - private Duration initialDelay = Duration.ofSeconds(5); - private Duration maxDelay = Duration.ofSeconds(300); - private double backoffRate = 1.5; - private JitterStrategy jitter = JitterStrategy.FULL; - - public Builder maxAttempts(int maxAttempts) { ... } - public Builder initialDelay(Duration initialDelay) { ... } - public Builder maxDelay(Duration maxDelay) { ... } - public Builder backoffRate(double backoffRate) { ... } - public Builder jitter(JitterStrategy jitter) { ... } - public WaitForConditionWaitStrategy build() { ... } + public static class Presets { + public static final WaitForConditionWaitStrategy DEFAULT = ...; } + + public static WaitForConditionWaitStrategy defaultStrategy(); + + public static WaitForConditionWaitStrategy exponentialBackoff( + int maxAttempts, Duration initialDelay, Duration maxDelay, + double backoffRate, JitterStrategy jitter); + + public static WaitForConditionWaitStrategy fixedDelay( + int maxAttempts, Duration fixedDelay); } ``` -The built strategy: -1. Calls `shouldContinuePolling.test(state)` — if false, returns `stopPolling()` -2. Checks `attempt >= maxAttempts` — if true, throws `WaitForConditionException` -3. Calculates delay: `min(initialDelay * backoffRate^(attempt-1), maxDelay)` -4. Applies jitter using the existing `JitterStrategy` enum -5. Ensures delay >= 1 second, rounds to nearest integer second -6. Returns `continuePolling(delay)` +Mirrors `RetryStrategies` with static factory methods and a `Presets` class. + +Default parameters (matching JS SDK): maxAttempts=60, initialDelay=5s, maxDelay=300s, backoffRate=1.5, jitter=FULL. + +Delay formula: `max(1, round(jitter(min(initialDelay × backoffRate^attempt, maxDelay))))` -### WaitForConditionConfig +Validation: maxAttempts > 0, initialDelay >= 1s, maxDelay >= 1s, backoffRate >= 1.0, jitter not null. + +### WaitForConditionConfig\ ```java public class WaitForConditionConfig { - private final WaitForConditionWaitStrategy waitStrategy; - private final T initialState; - private final SerDes serDes; // nullable, falls back to DurableConfig default + public static Builder builder(); - public static Builder builder(WaitForConditionWaitStrategy waitStrategy, T initialState) { ... } + public WaitForConditionWaitStrategy waitStrategy(); // defaults to WaitStrategies.defaultStrategy() + public SerDes serDes(); // defaults to null (uses handler default) + public Builder toBuilder(); // for internal SerDes injection public static class Builder { - public Builder serDes(SerDes serDes) { ... } - public WaitForConditionConfig build() { ... } + public Builder waitStrategy(WaitForConditionWaitStrategy waitStrategy); + public Builder serDes(SerDes serDes); + public WaitForConditionConfig build(); } } ``` -`waitStrategy` and `initialState` are required constructor parameters on the builder (not optional setters), so they can never be null. +Holds only optional parameters. Required parameters (`initialState`, `checkFunc`) are direct method arguments on `DurableContext.waitForCondition()`. -### WaitForConditionOperation +### DurableContext API (8 signatures) -Extends `BaseDurableOperation`. Key behaviors: +Delegation chain (same pattern as `step()`): +- All sync methods → corresponding async method → `.get()` +- All Class-based methods → TypeToken-based via `TypeToken.get(resultType)` +- All no-config methods → config method with `WaitForConditionConfig.builder().build()` +- Core method: `waitForConditionAsync(name, TypeToken, checkFunc, initialState, config)` -- **start()**: Begins the check loop from `initialState` at attempt 0 -- **replay(existing)**: Handles all operation statuses (SUCCEEDED, FAILED, PENDING, STARTED, READY) -- **executeCheckLoop(state, attempt)**: Core polling logic - - Creates a `StepContext` for the check function - - Executes check function in the user executor (same pattern as `StepOperation`) - - Serializes/deserializes state through SerDes (round-trip, matching JS SDK) - - Calls wait strategy with deserialized state - - Checkpoints RETRY with `NextAttemptDelaySeconds` or SUCCEED/FAIL -- **get()**: Blocks on completion, deserializes result or throws exception +The core method validates: `name` (via `ParameterValidator`), `typeToken` not null, `checkFunc` not null, `initialState` not null, `config` not null. -All checkpoint updates use `OperationType.STEP` and `OperationSubType.WAIT_FOR_CONDITION`. - -### DurableContext API Methods - -```java -// Sync methods (block until condition met) -public T waitForCondition(String name, Class resultType, - Function checkFunc, WaitForConditionConfig config) +### WaitForConditionOperation\ -public T waitForCondition(String name, TypeToken typeToken, - Function checkFunc, WaitForConditionConfig config) +Extends `BaseDurableOperation`. Key behaviors: -// Async methods (return DurableFuture immediately) -public DurableFuture waitForConditionAsync(String name, Class resultType, - Function checkFunc, WaitForConditionConfig config) +- **start()**: Begins the check loop from `initialState` at attempt 0 +- **replay(existing)**: Handles all operation statuses +- **resumeCheckLoop(existing)**: Deserializes checkpointed state (falls back to `initialState` if null, throws `SerDesException` if corrupt) +- **executeCheckLogic(state, attempt)**: Runs check function on user executor, handles `WaitForConditionResult`, checkpoints accordingly +- **get()**: Blocks on completion, deserializes result or reconstructs and throws the original exception -public DurableFuture waitForConditionAsync(String name, TypeToken typeToken, - Function checkFunc, WaitForConditionConfig config) -``` +All checkpoint updates use `OperationType.STEP` and `OperationSubType.WAIT_FOR_CONDITION`. -The check function signature is `Function` rather than `BiFunction` because the current state is managed internally by the operation. The check function receives the current state via the operation's internal loop — the `StepContext` provides logging and attempt info. Wait, actually looking at the JS SDK more carefully, the check function does receive the current state as a parameter: `(state: T, context) => Promise`. So the Java signature should be `BiFunction`. +### Error Handling -Corrected signature: +| Scenario | Behavior | +|----------|----------| +| Check function throws | Checkpoint FAIL, propagate via `get()` | +| Strategy throws `WaitForConditionException` | Checkpoint FAIL, propagate via `get()` | +| Checkpoint data fails to deserialize on replay | Throws `SerDesException` (propagates to handler) | +| `SuspendExecutionException` during check | Re-thrown (Lambda suspension) | +| `UnrecoverableDurableExecutionException` during check | Terminates execution | -```java -public DurableFuture waitForConditionAsync(String name, TypeToken typeToken, - BiFunction checkFunc, WaitForConditionConfig config) -``` +## Usage Examples -### OperationSubType Addition +### Minimal (default config) ```java -public enum OperationSubType { - RUN_IN_CHILD_CONTEXT("RunInChildContext"), - MAP("Map"), - PARALLEL("Parallel"), - WAIT_FOR_CALLBACK("WaitForCallback"), - WAIT_FOR_CONDITION("WaitForCondition"); // NEW - ... -} +var result = ctx.waitForCondition( + "wait-for-shipment", + String.class, + (status, stepCtx) -> { + var currentStatus = getOrderStatus(orderId); + return "SHIPPED".equals(currentStatus) + ? WaitForConditionResult.stopPolling(currentStatus) + : WaitForConditionResult.continuePolling(currentStatus); + }, + "PENDING"); ``` -### Error Handling - -- **Check function throws**: Checkpoint FAIL with serialized error, wrap in `WaitForConditionException` -- **Max attempts exceeded**: `WaitStrategies`-built strategy throws `WaitForConditionException("waitForCondition exceeded maximum attempts (N)")` -- **Custom strategy throws**: Propagated as-is (checkpoint FAIL) -- **SerDes failure**: Wrapped in `SerDesException` (existing pattern) - -A new `WaitForConditionException` extends `DurableOperationException` for domain-specific errors. - -### Exception Class +### Custom strategy ```java -public class WaitForConditionException extends DurableOperationException { - public WaitForConditionException(String message) { ... } - public WaitForConditionException(Operation operation) { ... } -} +var config = WaitForConditionConfig.builder() + .waitStrategy(WaitStrategies.fixedDelay(10, Duration.ofSeconds(30))) + .build(); + +var result = ctx.waitForCondition( + "wait-for-approval", + String.class, + (status, stepCtx) -> { + var current = checkApprovalStatus(requestId); + return "APPROVED".equals(current) + ? WaitForConditionResult.stopPolling(current) + : WaitForConditionResult.continuePolling(current); + }, + "PENDING_REVIEW", + config); ``` -## Testing Strategy - -### Unit Tests (sdk/src/test/) -- `WaitForConditionDecisionTest`: verify `continuePolling`/`stopPolling` factory methods -- `WaitStrategiesTest`: verify builder defaults, exponential backoff, jitter, max attempts -- `WaitForConditionConfigTest`: verify builder validation -- `WaitForConditionOperationTest`: verify start, replay, error handling - -### Integration Tests (sdk-integration-tests/) -- `WaitForConditionIntegrationTest`: end-to-end with `LocalDurableTestRunner`, verify replay across invocations - -### Example Tests (examples/) -- `WaitForConditionExample`: demonstrates polling with `WaitStrategies` factory -- `WaitForConditionExampleTest`: verifies example with `LocalDurableTestRunner` - -### Testing Framework -- JUnit 5 for all tests -- jqwik for property-based tests (already available in the project's test dependencies — if not, we'll use JUnit 5 parameterized tests with random generators) - -## Correctness Properties - -### Property 1: WaitForConditionWaitStrategy contract — stopPolling terminates -For any state `s` of type `T` and any attempt number `n >= 1`, if `waitStrategy.evaluate(s, n)` returns `StopPolling`, then `waitForCondition` completes with `s` as the result. - -**Validates: Requirements 1.5, 2.1** - -### Property 2: WaitStrategies factory — exponential backoff calculation -For any `initialDelay d`, `backoffRate r >= 1`, `maxDelay m >= d`, and attempt `n >= 1` with jitter=NONE, the delay equals `min(d * r^(n-1), m)` rounded to the nearest integer second, with a minimum of 1 second. - -**Validates: Requirements 2.3, 2.4** - -### Property 3: WaitStrategies factory — max attempts enforcement -For any `maxAttempts N >= 1` and any state where `shouldContinuePolling` returns true, calling the strategy with `attempt >= N` must throw `WaitForConditionException`. - -**Validates: Requirements 2.5** - -### Property 4: WaitForConditionConfig — required fields validation -Building a `WaitForConditionConfig` without a `waitStrategy` or with a null `initialState` must always throw an exception, regardless of other configuration. - -**Validates: Requirements 3.2** - -### Property 5: WaitForConditionWaitStrategy receives correct state and attempt -For any sequence of check function invocations, the wait strategy always receives the state returned by the most recent check function call and the correct 1-based attempt number. - -**Validates: Requirements 1.3, 2.1** - -### Property 6: Operation name validation -For any string that violates `ParameterValidator.validateOperationName` rules, calling `waitForCondition` or `waitForConditionAsync` with that name must throw. +## Testing -**Validates: Requirements 5.4** +### Unit Tests +- `WaitForConditionOperationTest`: replay (SUCCEEDED, FAILED, STARTED, READY, PENDING, unexpected status), null checkpoint data, corrupt checkpoint data +- `WaitStrategiesTest`: exponential backoff formula, max delay cap, max attempts enforcement, jitter bounds, validation, factory methods, presets +- `WaitForConditionConfigTest`: default strategy, custom strategy, SerDes, toBuilder -### Property 7: Jitter bounds -For any delay `d` and jitter strategy: NONE produces exactly `d`, FULL produces a value in `[0, d]` (clamped to min 1s), HALF produces a value in `[d/2, d]`. +### Integration Tests +- `WaitForConditionIntegrationTest`: basic polling, custom strategy, max attempts exceeded, check function error (with error type verification), replay across invocations, property tests for state/attempt correctness -**Validates: Requirements 2.3** +### Example +- `WaitForConditionExample`: simulates polling order shipment status (PENDING → PROCESSING → SHIPPED) diff --git a/examples/README.md b/examples/README.md index 48ba579dc..606aa6370 100644 --- a/examples/README.md +++ b/examples/README.md @@ -90,6 +90,7 @@ mvn test -Dtest=CloudBasedIntegrationTest \ | [WaitAtLeastInProcessExample](src/main/java/com/amazonaws/lambda/durable/examples/WaitAtLeastInProcessExample.java) | Wait completes before async step (no suspension) | | [ManyAsyncStepsExample](src/main/java/com/amazonaws/lambda/durable/examples/ManyAsyncStepsExample.java) | Performance test with 500 concurrent async steps | | [SimpleMapExample](src/main/java/com/amazonaws/lambda/durable/examples/SimpleMapExample.java) | Concurrent map over a collection with durable steps | +| [WaitForConditionExample](src/main/java/software/amazon/lambda/durable/examples/WaitForConditionExample.java) | Poll a condition until met with `waitForCondition()` | ## Cleanup diff --git a/examples/src/main/java/software/amazon/lambda/durable/examples/WaitForConditionExample.java b/examples/src/main/java/software/amazon/lambda/durable/examples/WaitForConditionExample.java index ca5240085..9c6f6b8da 100644 --- a/examples/src/main/java/software/amazon/lambda/durable/examples/WaitForConditionExample.java +++ b/examples/src/main/java/software/amazon/lambda/durable/examples/WaitForConditionExample.java @@ -2,31 +2,38 @@ // SPDX-License-Identifier: Apache-2.0 package software.amazon.lambda.durable.examples; +import java.util.concurrent.atomic.AtomicInteger; import software.amazon.lambda.durable.DurableContext; import software.amazon.lambda.durable.DurableHandler; -import software.amazon.lambda.durable.WaitForConditionResult; +import software.amazon.lambda.durable.model.WaitForConditionResult; /** * Example demonstrating the waitForCondition operation. * - *

Polls a counter until it reaches the length of the input name, then returns the final count. Uses the minimal API - * with default configuration — no explicit strategy or config needed. + *

This example simulates waiting for an order to ship, by repeatedly calling a check function. */ -public class WaitForConditionExample extends DurableHandler { +public class WaitForConditionExample extends DurableHandler { - @Override - public Integer handleRequest(GreetingRequest input, DurableContext context) { - var targetCount = input.getName().length(); + private final AtomicInteger callCount = new AtomicInteger(0); + @Override + public String handleRequest(String input, DurableContext context) { + // Poll the shipment status until the order is shipped. + // The check function simulates an order shipment status + // which transitions from PENDING > PROCESSING > SHIPPED return context.waitForCondition( - "count-to-name-length", - Integer.class, - (state, stepCtx) -> { - var next = state + 1; - return next >= targetCount - ? WaitForConditionResult.stopPolling(next) - : WaitForConditionResult.continuePolling(next); + "wait-for-shipment", + String.class, + (status, stepCtx) -> { + // Simulate checking shipment status from an external service + var count = callCount.incrementAndGet(); + if (count >= 3) { + // Order has shipped — stop polling + return WaitForConditionResult.stopPolling(input + ": SHIPPED"); + } + // Order still processing — continue polling + return WaitForConditionResult.continuePolling(input + ": PROCESSING"); }, - 0); + input + ": PENDING"); // Order pending - initial status } } diff --git a/examples/src/test/java/software/amazon/lambda/durable/examples/WaitForConditionExampleTest.java b/examples/src/test/java/software/amazon/lambda/durable/examples/WaitForConditionExampleTest.java index d135af1b5..32589f079 100644 --- a/examples/src/test/java/software/amazon/lambda/durable/examples/WaitForConditionExampleTest.java +++ b/examples/src/test/java/software/amazon/lambda/durable/examples/WaitForConditionExampleTest.java @@ -13,11 +13,11 @@ class WaitForConditionExampleTest { @Test void testWaitForConditionExample() { var handler = new WaitForConditionExample(); - var runner = LocalDurableTestRunner.create(GreetingRequest.class, handler); + var runner = LocalDurableTestRunner.create(String.class, handler); - var result = runner.runUntilComplete(new GreetingRequest("Alice")); + var result = runner.runUntilComplete("order-123"); assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus()); - assertEquals(5, result.getResult(Integer.class)); + assertEquals("order-123: SHIPPED", result.getResult(String.class)); } } diff --git a/sdk-integration-tests/src/test/java/software/amazon/lambda/durable/WaitForConditionIntegrationTest.java b/sdk-integration-tests/src/test/java/software/amazon/lambda/durable/WaitForConditionIntegrationTest.java index 5676b1b49..105dd6a88 100644 --- a/sdk-integration-tests/src/test/java/software/amazon/lambda/durable/WaitForConditionIntegrationTest.java +++ b/sdk-integration-tests/src/test/java/software/amazon/lambda/durable/WaitForConditionIntegrationTest.java @@ -11,6 +11,7 @@ import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import software.amazon.lambda.durable.model.ExecutionStatus; +import software.amazon.lambda.durable.model.WaitForConditionResult; import software.amazon.lambda.durable.retry.JitterStrategy; import software.amazon.lambda.durable.retry.WaitForConditionWaitStrategy; import software.amazon.lambda.durable.retry.WaitStrategies; @@ -129,6 +130,10 @@ void testCheckFunctionError() { var result = runner.runUntilComplete("test"); assertEquals(ExecutionStatus.FAILED, result.getStatus()); + var error = result.getError(); + assertTrue(error.isPresent(), "Error should be present"); + assertEquals("java.lang.IllegalStateException", error.get().errorType()); + assertEquals("Check function failed", error.get().errorMessage()); } @Test @@ -172,7 +177,7 @@ void testReplayAcrossInvocations() { assertEquals(firstCheckCount, checkCount.get()); } - // ---- PBT — isDone=true completes with that state as result ---- + // ---- isDone=true completes with that state as result ---- @RepeatedTest(50) void propertyStopPollingCompletesWithState() { @@ -207,7 +212,7 @@ void propertyStopPollingCompletesWithState() { assertEquals(target, result.getResult(Integer.class)); } - // ---- PBT — wait strategy receives correct state and attempt ---- + // ---- wait strategy receives correct state and attempt ---- @RepeatedTest(50) void propertyWaitStrategyReceivesCorrectStateAndAttempt() { diff --git a/sdk/src/main/java/software/amazon/lambda/durable/DurableContext.java b/sdk/src/main/java/software/amazon/lambda/durable/DurableContext.java index 90179bcdb..ffefc6e11 100644 --- a/sdk/src/main/java/software/amazon/lambda/durable/DurableContext.java +++ b/sdk/src/main/java/software/amazon/lambda/durable/DurableContext.java @@ -9,6 +9,7 @@ import java.util.function.Function; import java.util.function.Supplier; import software.amazon.lambda.durable.model.MapResult; +import software.amazon.lambda.durable.model.WaitForConditionResult; public interface DurableContext extends BaseContext { /** diff --git a/sdk/src/main/java/software/amazon/lambda/durable/context/DurableContextImpl.java b/sdk/src/main/java/software/amazon/lambda/durable/context/DurableContextImpl.java index f6a510191..e2e44a777 100644 --- a/sdk/src/main/java/software/amazon/lambda/durable/context/DurableContextImpl.java +++ b/sdk/src/main/java/software/amazon/lambda/durable/context/DurableContextImpl.java @@ -28,7 +28,6 @@ import software.amazon.lambda.durable.TypeToken; import software.amazon.lambda.durable.WaitForCallbackConfig; import software.amazon.lambda.durable.WaitForConditionConfig; -import software.amazon.lambda.durable.WaitForConditionResult; import software.amazon.lambda.durable.execution.ExecutionManager; import software.amazon.lambda.durable.execution.OperationIdGenerator; import software.amazon.lambda.durable.execution.ThreadType; @@ -36,6 +35,7 @@ import software.amazon.lambda.durable.model.MapResult; import software.amazon.lambda.durable.model.OperationIdentifier; import software.amazon.lambda.durable.model.OperationSubType; +import software.amazon.lambda.durable.model.WaitForConditionResult; import software.amazon.lambda.durable.operation.CallbackOperation; import software.amazon.lambda.durable.operation.ChildContextOperation; import software.amazon.lambda.durable.operation.InvokeOperation; diff --git a/sdk/src/main/java/software/amazon/lambda/durable/WaitForConditionResult.java b/sdk/src/main/java/software/amazon/lambda/durable/model/WaitForConditionResult.java similarity index 96% rename from sdk/src/main/java/software/amazon/lambda/durable/WaitForConditionResult.java rename to sdk/src/main/java/software/amazon/lambda/durable/model/WaitForConditionResult.java index 74346110a..67c073c82 100644 --- a/sdk/src/main/java/software/amazon/lambda/durable/WaitForConditionResult.java +++ b/sdk/src/main/java/software/amazon/lambda/durable/model/WaitForConditionResult.java @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -package software.amazon.lambda.durable; +package software.amazon.lambda.durable.model; /** * Result returned by a WaitForCondition check function to signal whether the condition is met. diff --git a/sdk/src/main/java/software/amazon/lambda/durable/operation/WaitForConditionOperation.java b/sdk/src/main/java/software/amazon/lambda/durable/operation/WaitForConditionOperation.java index 622ff94aa..700fe5f19 100644 --- a/sdk/src/main/java/software/amazon/lambda/durable/operation/WaitForConditionOperation.java +++ b/sdk/src/main/java/software/amazon/lambda/durable/operation/WaitForConditionOperation.java @@ -15,7 +15,6 @@ import software.amazon.lambda.durable.StepContext; import software.amazon.lambda.durable.TypeToken; import software.amazon.lambda.durable.WaitForConditionConfig; -import software.amazon.lambda.durable.WaitForConditionResult; import software.amazon.lambda.durable.context.DurableContextImpl; import software.amazon.lambda.durable.exception.DurableOperationException; import software.amazon.lambda.durable.exception.UnrecoverableDurableExecutionException; @@ -23,6 +22,7 @@ import software.amazon.lambda.durable.execution.SuspendExecutionException; import software.amazon.lambda.durable.model.OperationIdentifier; import software.amazon.lambda.durable.model.OperationSubType; +import software.amazon.lambda.durable.model.WaitForConditionResult; import software.amazon.lambda.durable.util.ExceptionHelper; /** diff --git a/sdk/src/main/java/software/amazon/lambda/durable/retry/WaitForConditionWaitStrategy.java b/sdk/src/main/java/software/amazon/lambda/durable/retry/WaitForConditionWaitStrategy.java index 27c365c7d..0d7ea2990 100644 --- a/sdk/src/main/java/software/amazon/lambda/durable/retry/WaitForConditionWaitStrategy.java +++ b/sdk/src/main/java/software/amazon/lambda/durable/retry/WaitForConditionWaitStrategy.java @@ -3,8 +3,8 @@ package software.amazon.lambda.durable.retry; import java.time.Duration; -import software.amazon.lambda.durable.WaitForConditionResult; import software.amazon.lambda.durable.exception.WaitForConditionException; +import software.amazon.lambda.durable.model.WaitForConditionResult; /** * Strategy that computes the delay before the next polling attempt in a {@code waitForCondition} operation. diff --git a/sdk/src/main/java/software/amazon/lambda/durable/retry/WaitStrategies.java b/sdk/src/main/java/software/amazon/lambda/durable/retry/WaitStrategies.java index a1b817c60..29da44a16 100644 --- a/sdk/src/main/java/software/amazon/lambda/durable/retry/WaitStrategies.java +++ b/sdk/src/main/java/software/amazon/lambda/durable/retry/WaitStrategies.java @@ -9,18 +9,11 @@ /** * Factory class for creating common {@link WaitForConditionWaitStrategy} implementations. * - *

Provides preset strategies for common use cases, as well as factory methods for creating custom strategies with - * exponential backoff and jitter. - * - *

Built-in strategies compute delays based solely on the attempt number, ignoring the state parameter. Custom - * strategies may use the state parameter to vary delays. + *

This class provides preset wait strategies (for use with waitForCondition) for common use cases, as well as + * factory methods for creating custom retry strategies with exponential backoff and jitter. */ public final class WaitStrategies { - private WaitStrategies() { - // Utility class - prevent instantiation - } - /** Preset wait strategies for common use cases. */ public static class Presets { @@ -34,8 +27,7 @@ public static class Presets { } /** - * Returns the default wait strategy: exponential backoff with 60 max attempts, 5s initial delay, 300s max delay, - * 1.5x backoff rate, and FULL jitter. + * Returns the default wait strategy. * * @param the type of state being polled * @return the default wait strategy diff --git a/sdk/src/test/java/software/amazon/lambda/durable/operation/WaitForConditionOperationTest.java b/sdk/src/test/java/software/amazon/lambda/durable/operation/WaitForConditionOperationTest.java index fb497203e..32f175ec1 100644 --- a/sdk/src/test/java/software/amazon/lambda/durable/operation/WaitForConditionOperationTest.java +++ b/sdk/src/test/java/software/amazon/lambda/durable/operation/WaitForConditionOperationTest.java @@ -6,6 +6,7 @@ import static org.mockito.Mockito.*; import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.BeforeEach; @@ -18,13 +19,15 @@ import software.amazon.lambda.durable.DurableConfig; import software.amazon.lambda.durable.TypeToken; import software.amazon.lambda.durable.WaitForConditionConfig; -import software.amazon.lambda.durable.WaitForConditionResult; import software.amazon.lambda.durable.context.DurableContextImpl; +import software.amazon.lambda.durable.exception.IllegalDurableOperationException; import software.amazon.lambda.durable.exception.NonDeterministicExecutionException; +import software.amazon.lambda.durable.exception.SerDesException; import software.amazon.lambda.durable.exception.WaitForConditionException; import software.amazon.lambda.durable.execution.ExecutionManager; import software.amazon.lambda.durable.execution.ThreadContext; import software.amazon.lambda.durable.execution.ThreadType; +import software.amazon.lambda.durable.model.WaitForConditionResult; import software.amazon.lambda.durable.serde.JacksonSerDes; class WaitForConditionOperationTest { @@ -276,4 +279,117 @@ void getFailedWithNullErrorDataThrowsStepFailedException() { assertThrows(WaitForConditionException.class, operation::get); } + + // ===== Replay PENDING ===== + + @Test + void replayPendingPollsAndResumesCheckLoop() throws Exception { + var pendingOp = Operation.builder() + .id(OPERATION_ID) + .name(OPERATION_NAME) + .type(OperationType.STEP) + .subType("WaitForCondition") + .status(OperationStatus.PENDING) + .stepDetails(StepDetails.builder().attempt(1).result("5").build()) + .build(); + when(executionManager.getOperationAndUpdateReplayState(OPERATION_ID)).thenReturn(pendingOp); + + var readyOp = Operation.builder() + .id(OPERATION_ID) + .name(OPERATION_NAME) + .type(OperationType.STEP) + .subType("WaitForCondition") + .status(OperationStatus.READY) + .stepDetails(StepDetails.builder().attempt(1).result("5").build()) + .build(); + when(executionManager.pollForOperationUpdates(OPERATION_ID)) + .thenReturn(CompletableFuture.completedFuture(readyOp)); + + var functionCalled = new AtomicBoolean(false); + var config = WaitForConditionConfig.builder().serDes(SERDES).build(); + var operation = createOperation( + (state, ctx) -> { + functionCalled.set(true); + return WaitForConditionResult.stopPolling(state); + }, + 0, + config); + + operation.execute(); + + Thread.sleep(200); + assertTrue(functionCalled.get(), "Check function should be called after PENDING → READY transition"); + } + + // ===== Replay unexpected status ===== + + @Test + void replayWithUnexpectedStatusTerminatesExecution() { + when(executionManager.getOperationAndUpdateReplayState(OPERATION_ID)) + .thenReturn(Operation.builder() + .id(OPERATION_ID) + .name(OPERATION_NAME) + .type(OperationType.STEP) + .subType("WaitForCondition") + .status(OperationStatus.UNKNOWN_TO_SDK_VERSION) + .build()); + + var config = WaitForConditionConfig.builder().serDes(SERDES).build(); + var operation = createOperation((state, ctx) -> WaitForConditionResult.stopPolling(state), 0, config); + + assertThrows(IllegalDurableOperationException.class, operation::execute); + } + + // ===== resumeCheckLoop with null checkpoint data ===== + + @Test + void replayStartedWithNullCheckpointDataUsesInitialState() throws Exception { + var op = Operation.builder() + .id(OPERATION_ID) + .name(OPERATION_NAME) + .type(OperationType.STEP) + .subType("WaitForCondition") + .status(OperationStatus.STARTED) + .stepDetails(StepDetails.builder().attempt(0).build()) // no result set + .build(); + when(executionManager.getOperationAndUpdateReplayState(OPERATION_ID)).thenReturn(op); + + var receivedState = new java.util.concurrent.atomic.AtomicInteger(-1); + var config = WaitForConditionConfig.builder().serDes(SERDES).build(); + var operation = createOperation( + (state, ctx) -> { + receivedState.set(state); + return WaitForConditionResult.stopPolling(state); + }, + 42, // initialState + config); + + operation.execute(); + + Thread.sleep(200); + assertEquals(42, receivedState.get(), "Should use initialState when checkpoint data is null"); + } + + // ===== resumeCheckLoop checkpoint deserialize exception ===== + + @Test + void replayStartedWithCorruptCheckpointDataThrowsSerDesException() { + var op = Operation.builder() + .id(OPERATION_ID) + .name(OPERATION_NAME) + .type(OperationType.STEP) + .subType("WaitForCondition") + .status(OperationStatus.STARTED) + .stepDetails(StepDetails.builder() + .attempt(1) + .result("not-valid-json!!!") + .build()) + .build(); + when(executionManager.getOperationAndUpdateReplayState(OPERATION_ID)).thenReturn(op); + + var config = WaitForConditionConfig.builder().serDes(SERDES).build(); + var operation = createOperation((state, ctx) -> WaitForConditionResult.stopPolling(state), 0, config); + + assertThrows(SerDesException.class, operation::execute); + } } diff --git a/sdk/src/test/java/software/amazon/lambda/durable/retry/WaitStrategiesTest.java b/sdk/src/test/java/software/amazon/lambda/durable/retry/WaitStrategiesTest.java index b16b43c2a..4ecec0fc0 100644 --- a/sdk/src/test/java/software/amazon/lambda/durable/retry/WaitStrategiesTest.java +++ b/sdk/src/test/java/software/amazon/lambda/durable/retry/WaitStrategiesTest.java @@ -126,10 +126,10 @@ void exponentialBackoff_withNullJitter_throwsIllegalArgumentException() { 60, Duration.ofSeconds(5), Duration.ofSeconds(300), 1.5, null)); } - // ---- PBT: Exponential backoff calculation with jitter=NONE ---- + // ---- Exponential backoff calculation with jitter=NONE ---- @RepeatedTest(100) - void pbt_exponentialBackoffCalculation_noJitter() { + void exponentialBackoffCalculation_noJitter() { var random = new Random(); long initialDelaySeconds = 1 + random.nextInt(30); @@ -157,10 +157,10 @@ void pbt_exponentialBackoffCalculation_noJitter() { initialDelaySeconds, backoffRate, maxDelaySeconds, attempt)); } - // ---- PBT: Max attempts enforcement ---- + // ---- Max attempts enforcement ---- @RepeatedTest(100) - void pbt_maxAttemptsEnforcement_throwsWhenExceeded() { + void maxAttemptsEnforcement_throwsWhenExceeded() { var random = new Random(); int maxAttempts = 1 + random.nextInt(50); @@ -177,10 +177,10 @@ void pbt_maxAttemptsEnforcement_throwsWhenExceeded() { assertTrue(exception.getMessage().contains("maximum attempts")); } - // ---- PBT: Jitter bounds ---- + // ---- Jitter bounds ---- @RepeatedTest(100) - void pbt_jitterBounds_noneProducesExactDelay() { + void jitterBounds_noneProducesExactDelay() { var random = new Random(); long delaySeconds = 1 + random.nextInt(300); @@ -192,7 +192,7 @@ void pbt_jitterBounds_noneProducesExactDelay() { } @RepeatedTest(100) - void pbt_jitterBounds_fullProducesDelayInRange() { + void jitterBounds_fullProducesDelayInRange() { var random = new Random(); long delaySeconds = 2 + random.nextInt(299); @@ -207,7 +207,7 @@ void pbt_jitterBounds_fullProducesDelayInRange() { } @RepeatedTest(100) - void pbt_jitterBounds_halfProducesDelayInRange() { + void jitterBounds_halfProducesDelayInRange() { var random = new Random(); long delaySeconds = 2 + random.nextInt(299);