diff --git a/docs/getting-started/key-concepts.md b/docs/getting-started/key-concepts.md index 2b1e487..41e9c61 100644 --- a/docs/getting-started/key-concepts.md +++ b/docs/getting-started/key-concepts.md @@ -122,11 +122,21 @@ durable operation calls on every invocation. ## Determinism Because your code runs again on replay, it must be **deterministic**. Deterministic -means that the code always produces the same results given the same inputs. Given the -same inputs and checkpoint log, your function must make the same sequence of durable -operation calls. Avoid operations with side effects (like generating random numbers or -getting the current time) outside of steps, as these can produce different values during -replay and cause non-deterministic behavior. +means that the code always produces the same results given the same inputs. During +replay, your function runs from the beginning and must follow the same execution path as +the original run. Given the same inputs and checkpoint log, your function must make the +same sequence of durable operation calls. Avoid operations with side effects outside of +steps, as these can produce different values during replay and cause non-deterministic +behavior. + +These are some examples of non-deterministic code: + +- Random number generation and UUIDs +- Current time or timestamps +- External API calls and database queries +- File system operations + +Wrap such non-deterministic code in [steps](../sdk-reference/operations/step.md). ### Rules for deterministic durable operations diff --git a/docs/sdk-reference/error-handling/retries.md b/docs/sdk-reference/error-handling/retries.md index 19075b1..d50352c 100644 --- a/docs/sdk-reference/error-handling/retries.md +++ b/docs/sdk-reference/error-handling/retries.md @@ -1,144 +1,94 @@ -# Retries +# Retry strategies -## Table of Contents +Retry strategies configure how the SDK responds to failures in steps. You control the +number of attempts, delay between retries, backoff rate, and which exceptions trigger a +retry. If no retry strategy is configured on a step, any exception propagates +immediately and fails the execution. -- [Overview](#overview) -- [Creating retry strategies](#creating-retry-strategies) -- [RetryStrategyConfig parameters](#retrystrategyconfig-parameters) -- [Retry presets](#retry-presets) -- [Retrying specific exceptions](#retrying-specific-exceptions) -- [Exponential backoff](#exponential-backoff) +## Creating a retry strategy -[← Back to main index](../index.md) - -## Overview - -Retry strategies configure how the SDK responds to transient failures in steps. You can control the number of attempts, delay between retries, backoff rate, and which exceptions trigger a retry. - -[↑ Back to top](#table-of-contents) - -## Creating retry strategies - -Use `RetryStrategyConfig` to define retry behavior: +Use `createRetryStrategy()` (TypeScript/Python) or +`RetryStrategies.exponentialBackoff()` (Java) to build a strategy, then pass it to +`StepConfig`: === "TypeScript" - ``` typescript - --8<-- "examples/typescript/core/steps/unreliable-operation.ts" + ```typescript + --8<-- "examples/typescript/advanced/error-handling/exponential-backoff.ts" ``` === "Python" - ``` python - --8<-- "examples/python/core/steps/unreliable-operation.py" + ```python + --8<-- "examples/python/advanced/error-handling/exponential-backoff.py" ``` === "Java" - ``` java - --8<-- "examples/java/core/steps/unreliable-operation.java" + ```java + --8<-- "examples/java/advanced/error-handling/exponential-backoff.java" ``` -[↑ Back to top](#table-of-contents) - ## RetryStrategyConfig parameters -**max_attempts** - Maximum number of attempts (including the initial attempt). Default: 3. +**max_attempts / maxAttempts** Maximum number of attempts including the initial attempt. +Default: 3. -**initial_delay_seconds** - Initial delay before first retry in seconds. Default: 5. +**initial_delay / initialDelay** Delay before the first retry. Default: 5 seconds. -**max_delay_seconds** - Maximum delay between retries in seconds. Default: 300 (5 minutes). +**max_delay / maxDelay** Maximum delay between retries. Default: 5 minutes. -**backoff_rate** - Multiplier for exponential backoff. Default: 2.0. +**backoff_rate / backoffRate** Multiplier for exponential backoff. Default: 2.0. -**jitter_strategy** - Jitter strategy to add randomness to delays. Default: `JitterStrategy.FULL`. +**jitter_strategy / jitter** Jitter strategy to spread retries. Default: `FULL`. -**retryable_errors** - List of error message patterns to retry (strings or regex patterns). Default: matches all errors. +**retryable_errors / retryableErrors** Error message patterns to retry (strings or +regex). Default: matches all errors. -**retryable_error_types** - List of exception types to retry. Default: empty (retry all). - -[↑ Back to top](#table-of-contents) +**retryable_error_types / retryableErrorTypes** Exception types to retry. Default: empty +(retries all). ## Retry presets -The SDK provides preset retry strategies for common scenarios: - === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/advanced/error-handling/retry-presets.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/advanced/error-handling/retry-presets.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/advanced/error-handling/retry-presets.java" ``` -[↑ Back to top](#table-of-contents) - ## Retrying specific exceptions -Only retry certain exception types: - === "TypeScript" - ``` typescript - --8<-- "examples/typescript/advanced/error-handling/retry-specific-exceptions.ts" + ```typescript + --8<-- "examples/typescript/sdk-reference/error-handling/unreliable-operation.ts" ``` === "Python" - ``` python - --8<-- "examples/python/advanced/error-handling/retry-specific-exceptions.py" + ```python + --8<-- "examples/python/sdk-reference/error-handling/unreliable-operation.py" ``` === "Java" - ``` java - --8<-- "examples/java/advanced/error-handling/retry-specific-exceptions.java" + ```java + --8<-- "examples/java/sdk-reference/error-handling/unreliable-operation.java" ``` -[↑ Back to top](#table-of-contents) - -## Exponential backoff - -Configure exponential backoff to avoid overwhelming failing services: - -=== "TypeScript" - - ``` typescript - --8<-- "examples/typescript/advanced/error-handling/exponential-backoff.ts" - ``` - -=== "Python" - - ``` python - --8<-- "examples/python/advanced/error-handling/exponential-backoff.py" - ``` - -=== "Java" - - ``` java - --8<-- "examples/java/advanced/error-handling/exponential-backoff.java" - ``` - -With this configuration: -- Attempt 1: Immediate -- Attempt 2: After 1 second -- Attempt 3: After 2 seconds -- Attempt 4: After 4 seconds -- Attempt 5: After 8 seconds - -[↑ Back to top](#table-of-contents) - ## See also -- [Errors](errors.md) - Exception types and error responses -- [Step](../operations/step.md) - Configure retry for steps +- [Errors](errors.md) +- [Steps](../operations/step.md) diff --git a/docs/sdk-reference/operations/step.md b/docs/sdk-reference/operations/step.md index 187d472..36e8138 100644 --- a/docs/sdk-reference/operations/step.md +++ b/docs/sdk-reference/operations/step.md @@ -1,582 +1,433 @@ # Steps -## Table of Contents +## Checkpointed results -- [What are steps?](#what-are-steps) -- [Terminology](#terminology) -- [Key features](#key-features) -- [Getting started](#getting-started) -- [Method signature](#method-signature) -- [Using the @durable_step decorator](#using-the-durable_step-decorator) -- [Naming steps](#naming-steps) -- [Configuration](#configuration) -- [Advanced patterns](#advanced-patterns) -- [Best practices](#best-practices) -- [FAQ](#faq) -- [Testing](#testing) -- [See also](#see-also) +A step executes the code you provide and checkpoints the result. On replay, the SDK +returns the checkpointed result rather than re-running the code inside the step. -[← Back to main index](../index.md) +Use steps to encapsulate any code that should not re-run once it has completed. -## Terminology +Wrapping non-deterministic code in steps is the primary way you ensure that your durable +execution is [deterministic](../../getting-started/key-concepts.md#determinism). +Non-deterministic code includes fetching the current time, generating a random number or +UUID, causing side-effects such as writing to disk, or calling an API that might return +a different result on different calls. -**Step** - A durable operation that executes a function and checkpoints its result. Created using `context.step()`. +When you encapsulate such code in a step it becomes deterministic in your durable +execution because the step doesn’t generate different results on replay. -**Step function** - A function decorated with `@durable_step` that can be executed as a step. Receives a `StepContext` as its first parameter. - -**Checkpoint** - A saved state of execution that allows your function to resume from a specific point. The SDK creates checkpoints automatically after each step completes. - -**Replay** - The process of re-executing your function code when resuming from a checkpoint. Completed steps return their saved results instantly without re-executing. - -**Step semantics** - Controls how many times a step executes per retry attempt. At-least-once (default) re-executes on retry. At-most-once executes only once per retry attempt. - -**StepContext** - A context object passed to step functions containing metadata about the current execution. - -[↑ Back to top](#table-of-contents) - -## What are steps? - -Steps are the fundamental building blocks of durable functions. A step is a unit of work that executes your code and automatically checkpoints the result. A completed step won't execute again, it returns its saved result instantly. If a step fails to complete, it automatically retries and saves the error after all retry attempts are exhausted. - -Use steps to: -- Execute business logic with automatic checkpointing -- Retry operations that might fail -- Control execution semantics (at-most-once or at-least-once) -- Break complex workflows into manageable units - -[↑ Back to top](#table-of-contents) - -## Key features - -- **Automatic checkpointing** - Results are saved automatically after execution -- **Configurable retry** - Define retry strategies with custom backoff -- **Execution semantics** - Choose at-most-once or at-least-once per retry -- **Named operations** - Identify steps by name for debugging and testing -- **Custom serialization** - Control how inputs and results are serialized -- **Instant replay** - Completed steps return saved results without re-executing - -[↑ Back to top](#table-of-contents) - -## Getting started - -Here's a simple example of using steps: +If a step fails during execution, it retries according to its configured retry strategy. +The step will checkpoint the last error after exhausting all retry attempts. === "TypeScript" - ``` typescript - --8<-- "examples/typescript/core/steps/add-numbers.ts" + ```typescript + --8<-- "examples/typescript/operations/steps/add-numbers.ts" ``` === "Python" - ``` python - --8<-- "examples/python/core/steps/add-numbers.py" + ```python + --8<-- "examples/python/operations/steps/add-numbers.py" ``` === "Java" - ``` java - --8<-- "examples/java/core/steps/add-numbers.java" + ```java + --8<-- "examples/java/operations/steps/add-numbers.java" ``` - -When this function runs: -1. `add_numbers(5, 3)` executes and returns 8 -2. The result is checkpointed automatically -3. If the durable function replays, the step returns 8 instantly without re-executing the `add_numbers` function - -[↑ Back to top](#table-of-contents) - ## Method signature -### context.step() +### step === "TypeScript" - ``` typescript - --8<-- "examples/typescript/core/steps/step-signature.ts" - ``` - -=== "Python" - - ``` python - --8<-- "examples/python/core/steps/step-signature.py" - ``` - -=== "Java" - - ``` java - --8<-- "examples/java/core/steps/step-signature.java" + ```typescript + --8<-- "examples/typescript/operations/steps/step-signature.ts" ``` + **Parameters:** -**Parameters:** - -- `func` - A callable that receives a `StepContext` and returns a result. Use the `@durable_step` decorator to create step functions. -- `name` (optional) - A name for the step, useful for debugging. If you decorate `func` with `@durable_step`, the SDK uses the function's name automatically. -- `config` (optional) - A `StepConfig` object to configure retry behavior, execution semantics, and serialization. - -**Returns:** The result of executing the step function. - -**Raises:** Any exception raised by the step function (after retries are exhausted if configured). - -[↑ Back to top](#table-of-contents) + - `name` (optional) A name for the step. Pass `undefined` to omit. + - `fn` A function that receives a `StepContext` and returns a `Promise`. + - `config` (optional) A `StepConfig` object. -## Using the @durable_step decorator + **Returns:** `DurablePromise`. Use `await` to get the result. -The `@durable_step` decorator marks a function as a step function. Step functions receive a `StepContext` as their first parameter: - -=== "TypeScript" - - ``` typescript - --8<-- "examples/typescript/core/steps/validate-order.ts" - ``` + **Throws:** `DurableOperationError` wrapping the original error after retries are + exhausted. `StepInterruptedError` if an at-most-once step was interrupted. === "Python" - ``` python - --8<-- "examples/python/core/steps/validate-order.py" + ```python + --8<-- "examples/python/operations/steps/step-signature.py" ``` -=== "Java" - - ``` java - --8<-- "examples/java/core/steps/validate-order.java" - ``` - - -**Why use @durable_step?** + **Parameters:** -The decorator wraps your function so it can be called with arguments and passed to `context.step()`. It also automatically uses the wrapped function's name as the step's name. You can optionally use lambda functions instead: + - `func` A callable that receives a `StepContext` and returns `T`. + - `name` (optional) A name for the step. Defaults to the function's name when using + `@durable_step`. + - `config` (optional) A `StepConfig` object. -=== "TypeScript" - - ``` typescript - --8<-- "examples/typescript/core/steps/lambda-step-no-name.ts" - ``` - -=== "Python" + **Returns:** `T`, the return value of `func`. - ``` python - --8<-- "examples/python/core/steps/lambda-step-no-name.py" - ``` + **Raises:** `CallableRuntimeError` wrapping the original exception after retries are + exhausted. `StepInterruptedError` if an at-most-once step was interrupted. === "Java" - ``` java - --8<-- "examples/java/core/steps/lambda-step-no-name.java" + ```java + --8<-- "examples/java/operations/steps/step-signature.java" ``` + **Parameters:** -**StepContext parameter:** - -The `StepContext` provides metadata about the current execution. While you must include it in your function signature, you typically don't need to use it unless you need execution metadata or custom logging. + - `name` (required) A name for the step. + - `resultType` The `Class` or `TypeToken` for deserialization. + - `func` A `Function` to execute. + - `config` (optional) A `StepConfig` object. -[↑ Back to top](#table-of-contents) + **Returns:** `T` (sync) or `DurableFuture` (async via `stepAsync()`). -## Naming steps + **Throws:** The original exception re-thrown after deserialization if possible, + otherwise `StepFailedException`. `StepInterruptedException` if an at-most-once step was + interrupted. -You can name steps explicitly using the `name` parameter. Named steps are easier to identify in logs and tests: +### StepConfig === "TypeScript" - ``` typescript - --8<-- "examples/typescript/core/steps/explicit-step-name.ts" + ```typescript + interface StepConfig { + retryStrategy?: (error: Error, attemptCount: number) => RetryDecision; + semantics?: StepSemantics; + serdes?: Serdes; + } ``` -=== "Python" - - ``` python - --8<-- "examples/python/core/steps/explicit-step-name.py" - ``` - -=== "Java" - - ``` java - --8<-- "examples/java/core/steps/explicit-step-name.java" - ``` - - -If you don't provide a name, the SDK uses the function's name automatically when using `@durable_step`: - -=== "TypeScript" + **Parameters:** - ``` typescript - --8<-- "examples/typescript/core/steps/process-payment.ts" - ``` + - `retryStrategy` (optional) A function returning a `RetryDecision`. Use + `createRetryStrategy()` to build one. See + [Retry strategies](../error-handling/retries.md). + - `semantics` (optional) `StepSemantics.AtLeastOncePerRetry` (default) or + `StepSemantics.AtMostOncePerRetry`. + - `serdes` (optional) Custom `Serdes` for the step result. See + [Serialization](../state/serialization.md). === "Python" - ``` python - --8<-- "examples/python/core/steps/process-payment.py" - ``` - -=== "Java" - - ``` java - --8<-- "examples/java/core/steps/process-payment.java" + ```python + @dataclass(frozen=True) + class StepConfig: + retry_strategy: Callable[[Exception, int], RetryDecision] | None = None + step_semantics: StepSemantics = StepSemantics.AT_LEAST_ONCE_PER_RETRY + serdes: SerDes | None = None ``` + **Parameters:** -**Naming best practices:** - -- Use descriptive names that explain what the step does -- Keep names consistent across your codebase -- Use names when you need to inspect specific steps in tests -- Let the SDK auto-name steps when using `@durable_step` - -**Note:** Names don't need to be unique, but using distinct names improves observability when debugging or monitoring your workflows. - -[↑ Back to top](#table-of-contents) - -## Configuration - -Configure step behavior using `StepConfig`: - -=== "TypeScript" - - ``` typescript - --8<-- "examples/typescript/core/steps/process-data.ts" - ``` - -=== "Python" - - ``` python - --8<-- "examples/python/core/steps/process-data.py" - ``` + - `retry_strategy` (optional) A callable returning a `RetryDecision`. Use + `create_retry_strategy()` to build one. See + [Retry strategies](../error-handling/retries.md). + - `step_semantics` (optional) `StepSemantics.AT_LEAST_ONCE_PER_RETRY` (default) or + `StepSemantics.AT_MOST_ONCE_PER_RETRY`. + - `serdes` (optional) Custom `SerDes` for the step result. See + [Serialization](../state/serialization.md). === "Java" - ``` java - --8<-- "examples/java/core/steps/process-data.java" + ```java + StepConfig.builder() + .retryStrategy(RetryStrategy) // optional + .semantics(StepSemantics) // optional + .serDes(SerDes) // optional + .build() ``` + **Parameters:** -### StepConfig parameters - -**retry_strategy** - A function that determines whether to retry after an exception. Use `create_retry_strategy()` to build one from `RetryStrategyConfig`. - -**step_semantics** - Controls execution behavior on retry: -- `AT_LEAST_ONCE_PER_RETRY` (default) - Step re-executes on each retry attempt -- `AT_MOST_ONCE_PER_RETRY` - Step executes only once per retry attempt, even if the function is replayed - -**serdes** - Custom serialization/deserialization for the step result. If not provided, uses JSON serialization. - -[↑ Back to top](#table-of-contents) + - `retryStrategy` (optional) A `RetryStrategy` instance. Use + `RetryStrategies.exponentialBackoff()` to build one. See + [Retry strategies](../error-handling/retries.md). + - `semantics` (optional) `StepSemantics.AT_LEAST_ONCE_PER_RETRY` (default) or + `StepSemantics.AT_MOST_ONCE_PER_RETRY`. + - `serDes` (optional) Custom `SerDes` for the step result. See + [Serialization](../state/serialization.md). -## Advanced patterns - -### Retry with exponential backoff - -Configure steps to retry with exponential backoff when they fail: +### StepContext === "TypeScript" - ``` typescript - --8<-- "examples/typescript/core/steps/exponential-backoff.ts" + ```typescript + interface StepContext { + logger: DurableContextLogger; + } ``` + - `logger` A logger enriched with execution context metadata. See + [Logging](../observability/logging.md). + === "Python" - ``` python - --8<-- "examples/python/core/steps/exponential-backoff.py" + ```python + @dataclass(frozen=True) + class StepContext: + logger: LoggerInterface ``` + - `logger` A logger enriched with execution context metadata. See + [Logging](../observability/logging.md). + === "Java" - ``` java - --8<-- "examples/java/core/steps/exponential-backoff.java" + ```java + interface StepContext { + DurableLogger getLogger(); + int getAttempt(); // current retry attempt, 0-based + boolean isReplaying(); + } ``` + - `getLogger()` A logger enriched with execution context metadata. See + [Logging](../observability/logging.md). + - `getAttempt()` The current retry attempt number (0-based). + - `isReplaying()` Whether the function is currently replaying from a checkpoint. -This configuration: -- Retries up to 3 times -- Waits 1 second before the first retry -- Doubles the wait time for each subsequent retry (2s, 4s, 8s) -- Caps the wait time at 10 seconds - -### Retry specific exceptions - -Only retry certain types of errors: +### StepSemantics === "TypeScript" - ``` typescript - --8<-- "examples/typescript/core/steps/unreliable-operation.ts" + ```typescript + enum StepSemantics { + AtLeastOncePerRetry = "AT_LEAST_ONCE_PER_RETRY", + AtMostOncePerRetry = "AT_MOST_ONCE_PER_RETRY", + } ``` + - `AtLeastOncePerRetry` (default) Re-executes the step if the function replays before + the result is checkpointed. Safe for idempotent operations. + - `AtMostOncePerRetry` Executes the step at most once per retry attempt. If the function + replays before the result is checkpointed, the SDK skips the step and raises + `StepInterruptedError`. Use for operations with side effects. + === "Python" - ``` python - --8<-- "examples/python/core/steps/unreliable-operation.py" + ```python + class StepSemantics(Enum): + AT_LEAST_ONCE_PER_RETRY = "AT_LEAST_ONCE_PER_RETRY" + AT_MOST_ONCE_PER_RETRY = "AT_MOST_ONCE_PER_RETRY" ``` + - `AT_LEAST_ONCE_PER_RETRY` (default) Re-execute the step if the function replays before + the result has checkpointed. Safe for idempotent operations. + - `AT_MOST_ONCE_PER_RETRY` Execute the step at most once per retry attempt. If the + function replays before the result has checkpointed, the SDK skips the step and + raises `StepInterruptedError`. Use for operations with side effects. + === "Java" - ``` java - --8<-- "examples/java/core/steps/unreliable-operation.java" + ```java + enum StepSemantics { + AT_LEAST_ONCE_PER_RETRY, + AT_MOST_ONCE_PER_RETRY + } ``` + - `AT_LEAST_ONCE_PER_RETRY` (default) Re-executes the step if the function replays + before the result is checkpointed. Safe for idempotent operations. + - `AT_MOST_ONCE_PER_RETRY` Executes the step at most once per retry attempt. If the + function replays before the result is checkpointed, the SDK skips the step and + throws `StepInterruptedException`. Use for operations with side effects. -### At-most-once semantics +## The Step's function -Use at-most-once semantics when your step has side effects that shouldn't be repeated: +A step function receives a `StepContext` as its first parameter. === "TypeScript" - ``` typescript - --8<-- "examples/typescript/core/steps/charge-credit-card.ts" - ``` - -=== "Python" - - ``` python - --8<-- "examples/python/core/steps/charge-credit-card.py" - ``` - -=== "Java" + Pass any async function directly. - ``` java - --8<-- "examples/java/core/steps/charge-credit-card.java" + ```typescript + --8<-- "examples/typescript/operations/steps/validate-order.ts" ``` - -With at-most-once semantics: -- The step executes only once per retry attempt -- If the function replays due to Lambda recycling, the step returns the saved result -- Use this for operations with side effects like payments, emails, or database writes - -### Multiple steps in sequence - -Chain multiple steps together to build complex workflows: - -=== "TypeScript" - - ``` typescript - --8<-- "examples/typescript/core/steps/fetch-user.ts" - ``` + Step functions are async. `await` the result of `context.step()`. === "Python" - ``` python - --8<-- "examples/python/core/steps/fetch-user.py" - ``` + Use the `@durable_step` decorator. It automatically uses the function's name as the step + name. Step functions must be synchronous. -=== "Java" - - ``` java - --8<-- "examples/java/core/steps/fetch-user.java" + ```python + --8<-- "examples/python/operations/steps/validate-order.py" ``` +=== "Java" -Each step is checkpointed independently. If the function is interrupted after step 1, it resumes at step 2 without re-fetching the user. - -[↑ Back to top](#table-of-contents) - -## Best practices - -**Use @durable_step for reusable functions** - Decorate functions you'll use as steps to get automatic naming and convenient with succinct syntax. - -**Name steps for debugging** - Use explicit names for steps you'll need to inspect in logs or tests. - -**Keep steps focused** - Each step should do one thing. Break complex operations into multiple steps. - -**Use retry for transient failures** - Configure retry strategies for operations that might fail temporarily (network calls, rate limits). - -**Choose semantics carefully** - Use at-most-once for operations with side effects. Use at-least-once (default) for idempotent operations. - -**Don't share state between steps** - Pass data between steps through return values, not global variables. - -**Wrap non-deterministic code in steps** - All non-deterministic code, such as random values or timestamps, must be wrapped in a step. Once the step completes, the result won't change on replay. - -**Handle errors explicitly** - Catch and handle exceptions in your step functions. Let retries handle transient failures. - -[↑ Back to top](#table-of-contents) - -## FAQ - -**Q: What's the difference between a step and a regular function call?** - -A: A step is checkpointed automatically. Completed steps return their saved results without re-executing. Regular function calls execute every time your function runs. - -**Q: When should I use at-most-once vs at-least-once semantics?** - -A: Use at-most-once for operations with side effects (payments, emails, database writes). Use at-least-once (default) for idempotent operations (calculations, data transformations). - -**Q: Can I use async functions as steps?** + Pass a lambda or method reference directly. Step functions are synchronous. Use + `stepAsync()` to get a `DurableFuture` you can compose with other async operations. -A: No, step functions must be synchronous. If you need to call async code, use `asyncio.run()` inside your step function. + ```java + --8<-- "examples/java/operations/steps/validate-order.java" + ``` -**Q: How do I pass multiple arguments to a step?** +### Anonymous step functions -A: Use the `@durable_step` decorator and pass arguments when calling the function: +You can also use inline lambdas. === "TypeScript" - ``` typescript - --8<-- "examples/typescript/core/steps/multi-argument-step.ts" + ```typescript + --8<-- "examples/typescript/operations/steps/lambda-step-no-name.ts" ``` === "Python" - ``` python - --8<-- "examples/python/core/steps/multi-argument-step.py" + If you use an anonymous function it will not automatically get named like the + `@durable_step` decorator does. + + ```python + --8<-- "examples/python/operations/steps/lambda-step-no-name.py" ``` === "Java" - ``` java - --8<-- "examples/java/core/steps/multi-argument-step.java" + ```java + --8<-- "examples/java/operations/steps/lambda-step-no-name.java" ``` - -**Q: Can I nest steps inside other steps?** - -A: No, you can't call `context.step()` inside a step function. Steps are leaf operations. Use child contexts if you need nested operations. - -**Q: What happens if a step raises an exception?** - -A: If no retry strategy is configured, the exception propagates and fails the execution. If retry is configured, the SDK retries according to your strategy. After exhausting retries, the step checkpoints the error and the exception propagates. - -**Q: How do I access the StepContext?** - -A: The `StepContext` is passed as the first parameter to your step function. It contains metadata about the execution, though you typically don't need to use it. - -**Q: Can I use lambda functions as steps?** - -A: Yes, but they won't have automatic names: +### Pass arguments to the step function === "TypeScript" - ``` typescript - --8<-- "examples/typescript/core/steps/lambda-step-example.ts" + Capture arguments in a closure: + + ```typescript + --8<-- "examples/typescript/operations/steps/multi-argument-step.ts" ``` === "Python" - ``` python - --8<-- "examples/python/core/steps/lambda-step-example.py" - ``` - -=== "Java" + Use `@durable_step` and pass arguments when calling the function: - ``` java - --8<-- "examples/java/core/steps/lambda-step-example.java" + ```python + --8<-- "examples/python/operations/steps/multi-argument-step.py" ``` +=== "Java" -Use `@durable_step` for better ergonomics. - -[↑ Back to top](#table-of-contents) + Capture arguments in a lambda: -## Testing + ```java + --8<-- "examples/java/operations/steps/multi-argument-step.java" + ``` -You can test steps using the testing SDK. The test runner executes your function and lets you inspect step results. +## Naming steps -### Basic step testing +Name your steps so they're easy to identify in logs and tests. Use descriptive names +that explain what the step does. Names don't need to be unique, but distinct names make +debugging easier. === "TypeScript" - ``` typescript - --8<-- "examples/typescript/core/steps/basic-step-testing.ts" - ``` + The name is the first argument. Pass `undefined` to omit it. === "Python" - ``` python - --8<-- "examples/python/core/steps/basic-step-testing.py" - ``` + The `@durable_step` decorator uses the function's name automatically as the step name. + Override it with the `name` keyword argument. === "Java" - ``` java - --8<-- "examples/java/core/steps/basic-step-testing.java" - ``` - + The name is always the first argument. Pass `null` for no name. -### Inspecting step results +## Configuration -Use `result.get_step()` to inspect individual step results: +Configure step behavior using `StepConfig`: === "TypeScript" - ``` typescript - --8<-- "examples/typescript/core/steps/test-error-handling.ts" + ```typescript + --8<-- "examples/typescript/operations/steps/process-data.ts" ``` === "Python" - ``` python - --8<-- "examples/python/core/steps/test-error-handling.py" + ```python + --8<-- "examples/python/operations/steps/process-data.py" ``` === "Java" - ``` java - --8<-- "examples/java/core/steps/test-error-handling.java" + ```java + --8<-- "examples/java/operations/steps/process-data.java" ``` +## Pass data between steps -### Testing retry behavior +Pass data between steps through return values. Do not use shared variables or closure +mutations. Steps return cached results on replay, so mutations to outer variables are +lost. -Test that steps retry correctly on failure: +### wrong way to pass data between steps === "TypeScript" - ``` typescript - --8<-- "examples/typescript/core/steps/test-retry-behavior.ts" + ```typescript + --8<-- "examples/typescript/operations/steps/passing-data-wrong.ts" ``` === "Python" - ``` python - --8<-- "examples/python/core/steps/test-retry-behavior.py" + ```python + --8<-- "examples/python/operations/steps/passing-data-wrong.py" ``` === "Java" - ``` java - --8<-- "examples/java/core/steps/test-retry-behavior.java" + ```java + --8<-- "examples/java/operations/steps/passing-data-wrong.java" ``` - -### Testing error handling - -Test that steps fail correctly when errors occur: +### correct way to pass data between steps === "TypeScript" - ``` typescript - --8<-- "examples/typescript/core/steps/inspect-step-results.ts" + ```typescript + --8<-- "examples/typescript/operations/steps/passing-data-correct.ts" ``` === "Python" - ``` python - --8<-- "examples/python/core/steps/inspect-step-results.py" + ```python + --8<-- "examples/python/operations/steps/passing-data-correct.py" ``` === "Java" - ``` java - --8<-- "examples/java/core/steps/inspect-step-results.java" + ```java + --8<-- "examples/java/operations/steps/passing-data-correct.java" ``` +## Nesting steps -For more testing patterns, see: -- [Basic tests](../testing-patterns/basic-tests.md) - Simple test examples -- [Complex workflows](../testing-patterns/complex-workflows.md) - Multi-step workflow testing -- [Best practices](../best-practices.md) - Testing recommendations +You cannot nest steps. Do not attempt to invoke another step from inside a step. If you +want to group or nest operations, use a [child context](child-context.md). -[↑ Back to top](#table-of-contents) +## Concurrency -## See also +Do not run steps concurrently. For concurrent operations, see [map](map.md) and +[parallel](parallel.md). -- [Retry strategies](../advanced/error-handling.md) - Implementing retry logic -- [Wait operations](wait.md) - Pause execution between steps -- [Child contexts](child-contexts.md) - Organize complex workflows -- [Examples](https://github.com/awslabs/aws-durable-execution-sdk-python/tree/main/examples/src/step) - More step examples +To code your own concurrency use a [child context](child-context.md) to encapsulate each +concurrent code path. -[↑ Back to top](#table-of-contents) +## See also -[↑ Back to top](#table-of-contents) +- [Retries](../error-handling/retries.md) +- [Testing](../../testing/basic-tests.md) +- [Wait operations](wait.md) +- [Child contexts](child-context.md) diff --git a/docs/testing/basic-tests.md b/docs/testing/basic-tests.md index fa1cbdb..d83d875 100644 --- a/docs/testing/basic-tests.md +++ b/docs/testing/basic-tests.md @@ -17,9 +17,15 @@ ## Overview -When you test durable functions, you need to verify that your function executed successfully, returned the expected result, and that operations like steps or waits ran correctly. This document shows you common patterns for writing these tests with simple assertions using the testing SDK. +When you test durable functions, you need to verify that your function executed +successfully, returned the expected result, and that operations like steps or waits ran +correctly. This document shows you common patterns for writing these tests with simple +assertions using the testing SDK. -The testing SDK (`aws-durable-execution-sdk-python-testing`) provides tools to run and inspect durable functions locally without deploying to AWS. Use these patterns as building blocks for your own tests, whether you're checking a simple calculation or inspecting individual operations. +The testing SDK (`aws-durable-execution-sdk-python-testing`) provides tools to run and +inspect durable functions locally without deploying to AWS. Use these patterns as +building blocks for your own tests, whether you're checking a simple calculation or +inspecting individual operations. [↑ Back to top](#table-of-contents) @@ -38,7 +44,8 @@ pip install aws-durable-execution-sdk-python-testing pip install pytest ``` -The core SDK provides the decorators and context for writing durable functions. The testing SDK provides the test runner and assertions for testing them. +The core SDK provides the decorators and context for writing durable functions. The +testing SDK provides the test runner and assertions for testing them. [↑ Back to top](#table-of-contents) @@ -61,31 +68,32 @@ my-project/ **Key files:** -- `src/my_function.py` - Contains your durable function with `@durable_execution` decorator +- `src/my_function.py` - Contains your durable function with `@durable_execution` + decorator - `test/conftest.py` - Configures the `durable_runner` fixture for pytest -- `test/test_my_function.py` - Contains your test cases using the `durable_runner` fixture +- `test/test_my_function.py` - Contains your test cases using the `durable_runner` + fixture **Example conftest.py:** === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/conftest-setup.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/conftest-setup.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/conftest-setup.java" ``` - [↑ Back to top](#table-of-contents) ## Getting started @@ -94,45 +102,44 @@ Here's a simple durable function: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/simple-function.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/simple-function.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/simple-function.java" ``` - And here's how you test it: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/simple-test.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/simple-test.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/simple-test.java" ``` - This test: + 1. Marks the test with `@pytest.mark.durable_execution` to configure the runner 2. Uses the `durable_runner` fixture to execute the function 3. Checks the execution status @@ -148,69 +155,66 @@ The most basic pattern verifies that your function completed successfully: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/check-success.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/check-success.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/check-success.java" ``` - ### Check for expected failures Test that your function fails correctly when given invalid input: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/check-expected-failures.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/check-expected-failures.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/check-expected-failures.java" ``` - ### Check execution with timeout Verify that your function completes within the expected time: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/check-timeout.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/check-timeout.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/check-timeout.java" ``` - [↑ Back to top](#table-of-contents) ## Result verification patterns @@ -221,69 +225,66 @@ Check that your function returns the expected value: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/verify-simple-values.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/verify-simple-values.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/verify-simple-values.java" ``` - ### Verify complex return values Check specific fields in complex return values: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/verify-complex-values.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/verify-complex-values.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/verify-complex-values.java" ``` - ### Verify list results Check that your function returns the expected list of values: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/verify-list-results.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/verify-list-results.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/verify-list-results.java" ``` - [↑ Back to top](#table-of-contents) ## Operation-specific assertions @@ -294,220 +295,210 @@ Here's a function with a step: === "TypeScript" - ``` typescript - --8<-- "examples/typescript/core/steps/add-numbers.ts" + ```typescript + --8<-- "examples/typescript/operations/steps/add-numbers.ts" ``` === "Python" - ``` python - --8<-- "examples/python/core/steps/add-numbers.py" + ```python + --8<-- "examples/python/operations/steps/add-numbers.py" ``` === "Java" - ``` java - --8<-- "examples/java/core/steps/add-numbers.java" + ```java + --8<-- "examples/java/operations/steps/add-numbers.java" ``` - Check that the step executed and produced the expected result: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/verify-step-operations.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/verify-step-operations.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/verify-step-operations.java" ``` - ### Verify wait operations Here's a function with a wait: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/wait-function.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/wait-function.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/wait-function.java" ``` - Check that the wait operation was created with correct timing: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/test-wait-operation.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/test-wait-operation.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/test-wait-operation.java" ``` - ### Verify callback operations Here's a function that creates a callback: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/callback-function.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/callback-function.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/callback-function.java" ``` - Check that the callback was created with correct configuration: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/verify-callback-operations.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/verify-callback-operations.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/verify-callback-operations.java" ``` - ### Verify child context operations Here's a function with a child context: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/child-context-function.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/child-context-function.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/child-context-function.java" ``` - Check that the child context executed correctly: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/verify-child-context.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/verify-child-context.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/verify-child-context.java" ``` - ### Verify parallel operations Here's a function with parallel operations: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/parallel-function.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/parallel-function.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/parallel-function.java" ``` - Check that multiple operations executed in parallel: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/verify-parallel-operations.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/verify-parallel-operations.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/verify-parallel-operations.java" ``` - [↑ Back to top](#table-of-contents) ## Test organization tips @@ -518,165 +509,165 @@ Name your tests to clearly describe what they verify: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/descriptive-test-names.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/descriptive-test-names.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/descriptive-test-names.java" ``` - ### Group related tests Organize tests by feature or functionality: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/group-related-tests.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/group-related-tests.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/group-related-tests.java" ``` - ### Use fixtures for common test data Create fixtures for test data you use across multiple tests: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/use-fixtures.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/use-fixtures.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/use-fixtures.java" ``` - ### Add docstrings to tests Document what each test verifies: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/add-docstrings.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/add-docstrings.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/add-docstrings.java" ``` - ### Use parametrized tests for similar cases Test multiple inputs with the same logic using `pytest.mark.parametrize`: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/parametrized-tests.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/parametrized-tests.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/parametrized-tests.java" ``` - ### Keep tests focused Each test should verify one specific behavior: === "TypeScript" - ``` typescript + ```typescript --8<-- "examples/typescript/testing-patterns/basic-tests/keep-tests-focused.ts" ``` === "Python" - ``` python + ```python --8<-- "examples/python/testing-patterns/basic-tests/keep-tests-focused.py" ``` === "Java" - ``` java + ```java --8<-- "examples/java/testing-patterns/basic-tests/keep-tests-focused.java" ``` - [↑ Back to top](#table-of-contents) ## FAQ **Q: Do I need to deploy my function to test it?** -A: No, the test runner executes your function locally. You only need to deploy for cloud testing mode. +A: No, the test runner executes your function locally. You only need to deploy for cloud +testing mode. **Q: How do I test functions with external dependencies?** -A: Mock external dependencies in your test setup. The test runner executes your function code as-is, so standard Python mocking works. +A: Mock external dependencies in your test setup. The test runner executes your function +code as-is, so standard Python mocking works. **Q: Can I test multiple functions in one test file?** -A: Yes, use different `@pytest.mark.durable_execution` markers for each function you want to test. +A: Yes, use different `@pytest.mark.durable_execution` markers for each function you +want to test. **Q: How do I access operation results?** -A: Use `result.get_step(name)` for steps, or iterate through `result.operations` to find specific operation types. +A: Use `result.get_step(name)` for steps, or iterate through `result.operations` to find +specific operation types. **Q: What's the difference between result.result and step.result?** -A: `result.result` is the final return value of your handler function. `step.result` is the return value of a specific step operation. +A: `result.result` is the final return value of your handler function. `step.result` is +the return value of a specific step operation. **Q: How do I test error scenarios?** -A: Check that `result.status is InvocationStatus.FAILED` and inspect `result.error` for the error message. +A: Check that `result.status is InvocationStatus.FAILED` and inspect `result.error` for +the error message. **Q: Can I run tests in parallel?** @@ -684,11 +675,13 @@ A: Yes, use pytest-xdist: `pytest -n auto` to run tests in parallel. **Q: How do I debug failing tests?** -A: Add print statements or use a debugger. The test runner executes your code locally, so standard debugging tools work. +A: Add print statements or use a debugger. The test runner executes your code locally, +so standard debugging tools work. **Q: What timeout should I use?** -A: Use a timeout slightly longer than your function's expected execution time. For most tests, 10-30 seconds is sufficient. +A: Use a timeout slightly longer than your function's expected execution time. For most +tests, 10-30 seconds is sufficient. **Q: How do I test functions that use environment variables?** diff --git a/examples/java/advanced/error-handling/exponential-backoff.java b/examples/java/advanced/error-handling/exponential-backoff.java index 48cdd22..338f241 100644 --- a/examples/java/advanced/error-handling/exponential-backoff.java +++ b/examples/java/advanced/error-handling/exponential-backoff.java @@ -1 +1,27 @@ -// Coming soon... +import java.time.Duration; +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; +import software.amazon.lambda.durable.StepContext; +import software.amazon.lambda.durable.config.StepConfig; +import software.amazon.lambda.durable.retry.JitterStrategy; +import software.amazon.lambda.durable.retry.RetryStrategies; + +public class ExponentialBackoffExample extends DurableHandler { + @Override + public String handleRequest(Object input, DurableContext context) { + StepConfig config = StepConfig.builder() + .retryStrategy(RetryStrategies.exponentialBackoff( + 3, + Duration.ofSeconds(1), + Duration.ofSeconds(10), + 2.0, + JitterStrategy.FULL)) + .build(); + + String result = context.step("retry_step", String.class, + (StepContext ctx) -> "Step with exponential backoff", + config); + + return "Result: " + result; + } +} diff --git a/examples/java/core/steps/add-numbers.java b/examples/java/core/steps/add-numbers.java deleted file mode 100644 index 48cdd22..0000000 --- a/examples/java/core/steps/add-numbers.java +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/java/core/steps/charge-credit-card.java b/examples/java/core/steps/charge-credit-card.java deleted file mode 100644 index 48cdd22..0000000 --- a/examples/java/core/steps/charge-credit-card.java +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/java/core/steps/explicit-step-name.java b/examples/java/core/steps/explicit-step-name.java deleted file mode 100644 index 48cdd22..0000000 --- a/examples/java/core/steps/explicit-step-name.java +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/java/core/steps/exponential-backoff.java b/examples/java/core/steps/exponential-backoff.java deleted file mode 100644 index 48cdd22..0000000 --- a/examples/java/core/steps/exponential-backoff.java +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/java/core/steps/fetch-user.java b/examples/java/core/steps/fetch-user.java deleted file mode 100644 index 48cdd22..0000000 --- a/examples/java/core/steps/fetch-user.java +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/java/core/steps/lambda-step-example.java b/examples/java/core/steps/lambda-step-example.java index 48cdd22..9824127 100644 --- a/examples/java/core/steps/lambda-step-example.java +++ b/examples/java/core/steps/lambda-step-example.java @@ -1 +1,13 @@ -// Coming soon... +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; +import software.amazon.lambda.durable.StepContext; + +public class LambdaStepExample extends DurableHandler { + @Override + public String handleRequest(Object input, DurableContext context) { + // Java always requires a name — use a descriptive string + String result = context.step("my_step", String.class, + (StepContext ctx) -> "some value"); + return result; + } +} diff --git a/examples/java/core/steps/lambda-step-no-name.java b/examples/java/core/steps/lambda-step-no-name.java deleted file mode 100644 index 48cdd22..0000000 --- a/examples/java/core/steps/lambda-step-no-name.java +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/java/core/steps/multi-argument-step.java b/examples/java/core/steps/multi-argument-step.java deleted file mode 100644 index 48cdd22..0000000 --- a/examples/java/core/steps/multi-argument-step.java +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/java/core/steps/process-data.java b/examples/java/core/steps/process-data.java deleted file mode 100644 index 48cdd22..0000000 --- a/examples/java/core/steps/process-data.java +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/java/core/steps/process-payment.java b/examples/java/core/steps/process-payment.java deleted file mode 100644 index 48cdd22..0000000 --- a/examples/java/core/steps/process-payment.java +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/java/core/steps/step-signature.java b/examples/java/core/steps/step-signature.java deleted file mode 100644 index 48cdd22..0000000 --- a/examples/java/core/steps/step-signature.java +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/java/core/steps/unreliable-operation.java b/examples/java/core/steps/unreliable-operation.java deleted file mode 100644 index 48cdd22..0000000 --- a/examples/java/core/steps/unreliable-operation.java +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/java/core/steps/validate-order.java b/examples/java/core/steps/validate-order.java deleted file mode 100644 index 48cdd22..0000000 --- a/examples/java/core/steps/validate-order.java +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/java/operations/steps/add-numbers.java b/examples/java/operations/steps/add-numbers.java new file mode 100644 index 0000000..4ed9b3e --- /dev/null +++ b/examples/java/operations/steps/add-numbers.java @@ -0,0 +1,12 @@ +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; +import software.amazon.lambda.durable.StepContext; + +public class AddNumbersExample extends DurableHandler { + @Override + public Integer handleRequest(Object input, DurableContext context) { + int result = context.step("add_numbers", Integer.class, + (StepContext ctx) -> 5 + 3); + return result; + } +} diff --git a/examples/java/operations/steps/lambda-step-no-name.java b/examples/java/operations/steps/lambda-step-no-name.java new file mode 100644 index 0000000..f8fd571 --- /dev/null +++ b/examples/java/operations/steps/lambda-step-no-name.java @@ -0,0 +1,13 @@ +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; +import software.amazon.lambda.durable.StepContext; + +public class LambdaStepNoNameExample extends DurableHandler { + @Override + public String handleRequest(Object input, DurableContext context) { + // Java requires a name — use a descriptive string + String result = context.step("my_step", String.class, + (StepContext ctx) -> "some value"); + return result; + } +} diff --git a/examples/java/operations/steps/multi-argument-step.java b/examples/java/operations/steps/multi-argument-step.java new file mode 100644 index 0000000..68d7982 --- /dev/null +++ b/examples/java/operations/steps/multi-argument-step.java @@ -0,0 +1,16 @@ +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; +import software.amazon.lambda.durable.StepContext; + +public class MultiArgumentStepExample extends DurableHandler { + @Override + public String handleRequest(Object input, DurableContext context) { + String arg1 = "value"; + int arg2 = 42; + + String result = context.step("my_step", String.class, + (StepContext ctx) -> arg1 + ": " + arg2); + + return result; + } +} diff --git a/examples/java/operations/steps/passing-data-correct.java b/examples/java/operations/steps/passing-data-correct.java new file mode 100644 index 0000000..f975cbb --- /dev/null +++ b/examples/java/operations/steps/passing-data-correct.java @@ -0,0 +1,31 @@ +import java.time.Duration; +import java.util.Map; +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; + +public class PassingDataCorrectExample extends DurableHandler, Void> { + + private String registerUser(String email) { + return "user-" + email; + } + + private void sendFollowUpEmail(String userId) { + // send email to user + } + + @Override + public Void handleRequest(Map event, DurableContext context) { + // ✅ CORRECT: userId is returned from the step and restored from checkpoint on replay + String userId = context.step("register-user", String.class, + ctx -> registerUser(event.get("email"))); + + context.wait("follow-up-delay", Duration.ofMinutes(10)); + + context.step("send-follow-up-email", Void.class, ctx -> { + sendFollowUpEmail(userId); // userId restored from checkpoint + return null; + }); + + return null; + } +} diff --git a/examples/java/operations/steps/passing-data-wrong.java b/examples/java/operations/steps/passing-data-wrong.java new file mode 100644 index 0000000..d3a7d7f --- /dev/null +++ b/examples/java/operations/steps/passing-data-wrong.java @@ -0,0 +1,36 @@ +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; + +public class PassingDataWrongExample extends DurableHandler, Void> { + + private String registerUser(String email) { + return "user-" + email; + } + + private void sendFollowUpEmail(String userId) { + // send email to user + } + + @Override + public Void handleRequest(Map event, DurableContext context) { + // ❌ WRONG: userId mutation is lost on replay after the wait + AtomicReference userId = new AtomicReference<>(""); + + context.step("register-user", String.class, ctx -> { + userId.set(registerUser(event.get("email"))); // ⚠️ Lost on replay! + return userId.get(); + }); + + context.wait("follow-up-delay", Duration.ofMinutes(10)); + + context.step("send-follow-up-email", Void.class, ctx -> { + sendFollowUpEmail(userId.get()); // userId is "" on replay + return null; + }); + + return null; + } +} diff --git a/examples/java/operations/steps/process-data.java b/examples/java/operations/steps/process-data.java new file mode 100644 index 0000000..1c3e4cb --- /dev/null +++ b/examples/java/operations/steps/process-data.java @@ -0,0 +1,28 @@ +import java.util.Map; +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; +import software.amazon.lambda.durable.StepContext; +import software.amazon.lambda.durable.config.StepConfig; +import software.amazon.lambda.durable.config.StepSemantics; +import software.amazon.lambda.durable.retry.RetryStrategies; + +public class ProcessDataExample extends DurableHandler, Map> { + @Override + public Map handleRequest(Map event, DurableContext context) { + StepConfig config = StepConfig.builder() + .retryStrategy(RetryStrategies.exponentialBackoff( + 3, + java.time.Duration.ofSeconds(1), + java.time.Duration.ofMinutes(5), + 2.0, + software.amazon.lambda.durable.retry.JitterStrategy.FULL)) + .semantics(StepSemantics.AT_LEAST_ONCE_PER_RETRY) + .build(); + + Map result = context.step("process_data", Map.class, + (StepContext ctx) -> Map.of("processed", event.get("data"), "status", "completed"), + config); + + return result; + } +} diff --git a/examples/java/operations/steps/process-payment.java b/examples/java/operations/steps/process-payment.java new file mode 100644 index 0000000..8cce8ee --- /dev/null +++ b/examples/java/operations/steps/process-payment.java @@ -0,0 +1,16 @@ +import java.util.Map; +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; +import software.amazon.lambda.durable.StepContext; + +public class ProcessPaymentExample extends DurableHandler, Map> { + @Override + public Map handleRequest(Map event, DurableContext context) { + double amount = (double) event.get("amount"); + + Map result = context.step("process_payment", Map.class, + (StepContext ctx) -> Map.of("status", "completed", "amount", amount)); + + return result; + } +} diff --git a/examples/java/operations/steps/step-signature.java b/examples/java/operations/steps/step-signature.java new file mode 100644 index 0000000..c9691c8 --- /dev/null +++ b/examples/java/operations/steps/step-signature.java @@ -0,0 +1,7 @@ +// Sync (blocks until complete) + T step(String name, Class resultType, Function func) + T step(String name, Class resultType, Function func, StepConfig config) + +// Async (returns a DurableFuture) + DurableFuture stepAsync(String name, Class resultType, Function func) + DurableFuture stepAsync(String name, Class resultType, Function func, StepConfig config) diff --git a/examples/java/operations/steps/validate-order.java b/examples/java/operations/steps/validate-order.java new file mode 100644 index 0000000..62e2ff1 --- /dev/null +++ b/examples/java/operations/steps/validate-order.java @@ -0,0 +1,16 @@ +import java.util.Map; +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; +import software.amazon.lambda.durable.StepContext; + +public class ValidateOrderExample extends DurableHandler, Map> { + @Override + public Map handleRequest(Map event, DurableContext context) { + String orderId = (String) event.get("order_id"); + + Map validation = context.step("validate_order", Map.class, + (StepContext ctx) -> Map.of("order_id", orderId, "valid", true)); + + return validation; + } +} diff --git a/examples/java/sdk-reference/error-handling/unreliable-operation.java b/examples/java/sdk-reference/error-handling/unreliable-operation.java new file mode 100644 index 0000000..06ceb14 --- /dev/null +++ b/examples/java/sdk-reference/error-handling/unreliable-operation.java @@ -0,0 +1,30 @@ +import java.time.Duration; +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; +import software.amazon.lambda.durable.StepContext; +import software.amazon.lambda.durable.config.StepConfig; +import software.amazon.lambda.durable.retry.JitterStrategy; +import software.amazon.lambda.durable.retry.RetryStrategies; + +public class UnreliableOperationExample extends DurableHandler { + @Override + public String handleRequest(Object input, DurableContext context) { + StepConfig config = StepConfig.builder() + .retryStrategy(RetryStrategies.exponentialBackoff( + 3, + Duration.ofSeconds(1), + Duration.ofMinutes(5), + 2.0, + JitterStrategy.FULL)) + .build(); + + String result = context.step("unreliable_operation", String.class, + (StepContext ctx) -> { + if (Math.random() > 0.5) throw new RuntimeException("Random error occurred"); + return "Operation succeeded"; + }, + config); + + return result; + } +} diff --git a/examples/java/testing-patterns/basic-tests/basic-step-testing.java b/examples/java/testing-patterns/basic-tests/basic-step-testing.java new file mode 100644 index 0000000..c39edac --- /dev/null +++ b/examples/java/testing-patterns/basic-tests/basic-step-testing.java @@ -0,0 +1,17 @@ +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import software.amazon.lambda.durable.model.ExecutionStatus; +import software.amazon.lambda.durable.testing.LocalDurableTestRunner; + +class BasicStepTestingTest { + @Test + void testStep() { + var runner = LocalDurableTestRunner.create(Object.class, new AddNumbersExample()); + + var result = runner.runUntilComplete(null); + + assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus()); + assertEquals(8, result.getResult(Integer.class)); + } +} diff --git a/examples/java/testing-patterns/basic-tests/inspect-step-results.java b/examples/java/testing-patterns/basic-tests/inspect-step-results.java new file mode 100644 index 0000000..a486f5b --- /dev/null +++ b/examples/java/testing-patterns/basic-tests/inspect-step-results.java @@ -0,0 +1,17 @@ +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import software.amazon.lambda.durable.model.ExecutionStatus; +import software.amazon.lambda.durable.testing.LocalDurableTestRunner; + +class InspectStepResultsErrorTest { + @Test + void testStepFailsWithError() { + var runner = LocalDurableTestRunner.create(Object.class, new FailingStepExample()); + + var result = runner.runUntilComplete(null); + + assertEquals(ExecutionStatus.FAILED, result.getStatus()); + assertTrue(result.getError().isPresent()); + } +} diff --git a/examples/java/testing-patterns/basic-tests/test-error-handling.java b/examples/java/testing-patterns/basic-tests/test-error-handling.java new file mode 100644 index 0000000..f08b04c --- /dev/null +++ b/examples/java/testing-patterns/basic-tests/test-error-handling.java @@ -0,0 +1,22 @@ +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.services.lambda.model.OperationStatus; +import software.amazon.lambda.durable.model.ExecutionStatus; +import software.amazon.lambda.durable.testing.LocalDurableTestRunner; + +class InspectStepResultsTest { + @Test + void testInspectStepResult() { + var runner = LocalDurableTestRunner.create(Object.class, new AddNumbersExample()); + + var result = runner.runUntilComplete(null); + + assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus()); + + var stepOp = runner.getOperation("add_numbers"); + assertNotNull(stepOp); + assertEquals(OperationStatus.SUCCEEDED, stepOp.getStatus()); + assertEquals(8, stepOp.getStepResult(Integer.class)); + } +} diff --git a/examples/java/testing-patterns/basic-tests/test-retry-behavior.java b/examples/java/testing-patterns/basic-tests/test-retry-behavior.java new file mode 100644 index 0000000..acae03f --- /dev/null +++ b/examples/java/testing-patterns/basic-tests/test-retry-behavior.java @@ -0,0 +1,21 @@ +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.services.lambda.model.OperationStatus; +import software.amazon.lambda.durable.model.ExecutionStatus; +import software.amazon.lambda.durable.testing.LocalDurableTestRunner; + +class TestRetryBehaviorTest { + @Test + void testStepRetriesAndSucceeds() { + var runner = LocalDurableTestRunner.create(Object.class, new UnreliableOperationExample()); + + var result = runner.runUntilComplete(null); + + assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus()); + + var stepOp = runner.getOperation("unreliable_operation"); + assertNotNull(stepOp); + assertEquals(OperationStatus.SUCCEEDED, stepOp.getStatus()); + } +} diff --git a/examples/python/advanced/error-handling/exponential-backoff.py b/examples/python/advanced/error-handling/exponential-backoff.py index ef7c312..b0d175b 100644 --- a/examples/python/advanced/error-handling/exponential-backoff.py +++ b/examples/python/advanced/error-handling/exponential-backoff.py @@ -1,6 +1,28 @@ -retry_config = RetryStrategyConfig( - max_attempts=5, - initial_delay_seconds=1, # First retry after 1 second - max_delay_seconds=60, # Cap at 60 seconds - backoff_rate=2.0, # Double delay each time: 1s, 2s, 4s, 8s, 16s... +from aws_durable_execution_sdk_python import ( + DurableContext, + durable_execution, ) +from aws_durable_execution_sdk_python.config import Duration, StepConfig +from aws_durable_execution_sdk_python.retries import ( + RetryStrategyConfig, + create_retry_strategy, +) + + +@durable_execution +def handler(event: dict, context: DurableContext) -> str: + retry_config = RetryStrategyConfig( + max_attempts=3, + initial_delay=Duration.from_seconds(1), + max_delay=Duration.from_seconds(10), + backoff_rate=2.0, + ) + + step_config = StepConfig(retry_strategy=create_retry_strategy(retry_config)) + + result = context.step( + lambda _: "Step with exponential backoff", + name="retry_step", + config=step_config, + ) + return f"Result: {result}" diff --git a/examples/python/core/steps/charge-credit-card.py b/examples/python/core/steps/charge-credit-card.py deleted file mode 100644 index b5ada39..0000000 --- a/examples/python/core/steps/charge-credit-card.py +++ /dev/null @@ -1,21 +0,0 @@ -from aws_durable_execution_sdk_python.config import StepConfig, StepSemantics - -@durable_step -def charge_credit_card(step_context: StepContext, amount: float) -> dict: - """Charge a credit card - should only happen once.""" - # Payment processing logic - return {"transaction_id": "txn_123", "status": "completed"} - -@durable_execution -def handler(event: dict, context: DurableContext) -> dict: - # Use at-most-once to prevent duplicate charges - step_config = StepConfig( - step_semantics=StepSemantics.AT_MOST_ONCE_PER_RETRY - ) - - payment = context.step( - charge_credit_card(event["amount"]), - config=step_config, - ) - - return payment diff --git a/examples/python/core/steps/explicit-step-name.py b/examples/python/core/steps/explicit-step-name.py deleted file mode 100644 index a0154da..0000000 --- a/examples/python/core/steps/explicit-step-name.py +++ /dev/null @@ -1,8 +0,0 @@ -@durable_execution -def handler(event: dict, context: DurableContext) -> str: - # Explicit name - result = context.step( - lambda _: "Step with explicit name", - name="custom_step" - ) - return f"Result: {result}" diff --git a/examples/python/core/steps/exponential-backoff.py b/examples/python/core/steps/exponential-backoff.py deleted file mode 100644 index 57cbf5a..0000000 --- a/examples/python/core/steps/exponential-backoff.py +++ /dev/null @@ -1,30 +0,0 @@ -from aws_durable_execution_sdk_python import ( - DurableContext, - durable_execution, -) -from aws_durable_execution_sdk_python.config import StepConfig -from aws_durable_execution_sdk_python.retries import ( - RetryStrategyConfig, - create_retry_strategy, -) - -@durable_execution -def handler(event: dict, context: DurableContext) -> str: - # Configure exponential backoff - retry_config = RetryStrategyConfig( - max_attempts=3, - initial_delay_seconds=1, - max_delay_seconds=10, - backoff_rate=2.0, - ) - - step_config = StepConfig( - retry_strategy=create_retry_strategy(retry_config) - ) - - result = context.step( - lambda _: "Step with exponential backoff", - name="retry_step", - config=step_config, - ) - return f"Result: {result}" diff --git a/examples/python/core/steps/fetch-user.py b/examples/python/core/steps/fetch-user.py deleted file mode 100644 index 3e64f1f..0000000 --- a/examples/python/core/steps/fetch-user.py +++ /dev/null @@ -1,36 +0,0 @@ -@durable_step -def fetch_user(step_context: StepContext, user_id: str) -> dict: - """Fetch user data.""" - return {"user_id": user_id, "name": "Jane Doe", "email": "jane_doe@example.com"} - -@durable_step -def validate_user(step_context: StepContext, user: dict) -> bool: - """Validate user data.""" - return user.get("email") is not None - -@durable_step -def send_notification(step_context: StepContext, user: dict) -> dict: - """Send notification to user.""" - return {"sent": True, "email": user["email"]} - -@durable_execution -def handler(event: dict, context: DurableContext) -> dict: - user_id = event["user_id"] - - # Step 1: Fetch user - user = context.step(fetch_user(user_id)) - - # Step 2: Validate user - is_valid = context.step(validate_user(user)) - - if not is_valid: - return {"status": "failed", "reason": "invalid_user"} - - # Step 3: Send notification - notification = context.step(send_notification(user)) - - return { - "status": "completed", - "user_id": user_id, - "notification_sent": notification["sent"], - } diff --git a/examples/python/core/steps/process-payment.py b/examples/python/core/steps/process-payment.py deleted file mode 100644 index a264419..0000000 --- a/examples/python/core/steps/process-payment.py +++ /dev/null @@ -1,9 +0,0 @@ -@durable_step -def process_payment(step_context: StepContext, amount: float) -> dict: - return {"status": "completed", "amount": amount} - -@durable_execution -def handler(event: dict, context: DurableContext) -> dict: - # Step is automatically named "process_payment" - result = context.step(process_payment(100.0)) - return result diff --git a/examples/python/core/steps/step-signature.py b/examples/python/core/steps/step-signature.py deleted file mode 100644 index 267fc42..0000000 --- a/examples/python/core/steps/step-signature.py +++ /dev/null @@ -1,5 +0,0 @@ -def step( - func: Callable[[StepContext], T], - name: str | None = None, - config: StepConfig | None = None, -) -> T diff --git a/examples/python/core/steps/add-numbers.py b/examples/python/operations/steps/add-numbers.py similarity index 81% rename from examples/python/core/steps/add-numbers.py rename to examples/python/operations/steps/add-numbers.py index 213f393..23bacec 100644 --- a/examples/python/core/steps/add-numbers.py +++ b/examples/python/operations/steps/add-numbers.py @@ -1,17 +1,17 @@ from aws_durable_execution_sdk_python import ( DurableContext, + StepContext, durable_execution, durable_step, - StepContext, ) + @durable_step def add_numbers(step_context: StepContext, a: int, b: int) -> int: - """Add two numbers together.""" return a + b + @durable_execution def handler(event: dict, context: DurableContext) -> int: - """Simple durable function with a step.""" result = context.step(add_numbers(5, 3)) return result diff --git a/examples/python/core/steps/lambda-step-no-name.py b/examples/python/operations/steps/lambda-step-no-name.py similarity index 100% rename from examples/python/core/steps/lambda-step-no-name.py rename to examples/python/operations/steps/lambda-step-no-name.py diff --git a/examples/python/core/steps/multi-argument-step.py b/examples/python/operations/steps/multi-argument-step.py similarity index 100% rename from examples/python/core/steps/multi-argument-step.py rename to examples/python/operations/steps/multi-argument-step.py diff --git a/examples/python/operations/steps/passing-data-correct.py b/examples/python/operations/steps/passing-data-correct.py new file mode 100644 index 0000000..d6cdafa --- /dev/null +++ b/examples/python/operations/steps/passing-data-correct.py @@ -0,0 +1,34 @@ +from aws_durable_execution_sdk_python import ( + DurableContext, + StepContext, + durable_execution, + durable_step, +) +from aws_durable_execution_sdk_python.config import Duration + + +def register_user(email: str) -> str: + return f"user-{email}" + + +def send_follow_up_email(user_id: str) -> None: + # send email to user + pass + + +# ✅ CORRECT: user_id is returned from the step and restored from checkpoint on replay +@durable_execution +def handler(event: dict, context: DurableContext) -> None: + @durable_step + def do_register(step_context: StepContext) -> str: + return register_user(event["email"]) + + user_id = context.step(do_register()) + + context.wait(Duration.from_minutes(10), name="follow-up-delay") + + @durable_step + def do_send(step_context: StepContext) -> None: + send_follow_up_email(user_id) # user_id restored from checkpoint + + context.step(do_send()) diff --git a/examples/python/operations/steps/passing-data-wrong.py b/examples/python/operations/steps/passing-data-wrong.py new file mode 100644 index 0000000..fbe4de4 --- /dev/null +++ b/examples/python/operations/steps/passing-data-wrong.py @@ -0,0 +1,37 @@ +from aws_durable_execution_sdk_python import ( + DurableContext, + StepContext, + durable_execution, + durable_step, +) +from aws_durable_execution_sdk_python.config import Duration + + +def register_user(email: str) -> str: + return f"user-{email}" + + +def send_follow_up_email(user_id: str) -> None: + # send email to user + pass + + +# ❌ WRONG: user_id mutation is lost on replay after the wait +@durable_execution +def handler(event: dict, context: DurableContext) -> None: + user_id = "" + + @durable_step + def do_register(step_context: StepContext) -> None: + nonlocal user_id + user_id = register_user(event["email"]) # ⚠️ Lost on replay! + + context.step(do_register()) + + context.wait(Duration.from_minutes(10), name="follow-up-delay") + + @durable_step + def do_send(step_context: StepContext) -> None: + send_follow_up_email(user_id) # user_id is "" on replay + + context.step(do_send()) diff --git a/examples/python/core/steps/process-data.py b/examples/python/operations/steps/process-data.py similarity index 82% rename from examples/python/core/steps/process-data.py rename to examples/python/operations/steps/process-data.py index de9ddae..b9ebe6d 100644 --- a/examples/python/core/steps/process-data.py +++ b/examples/python/operations/steps/process-data.py @@ -1,8 +1,8 @@ from aws_durable_execution_sdk_python import ( DurableContext, + StepContext, durable_execution, durable_step, - StepContext, ) from aws_durable_execution_sdk_python.config import StepConfig, StepSemantics from aws_durable_execution_sdk_python.retries import ( @@ -10,26 +10,23 @@ create_retry_strategy, ) + @durable_step def process_data(step_context: StepContext, data: str) -> dict: - """Process data with potential for transient failures.""" - # Your processing logic here return {"processed": data, "status": "completed"} + @durable_execution def handler(event: dict, context: DurableContext) -> dict: - # Create a retry strategy retry_config = RetryStrategyConfig( max_attempts=3, retryable_error_types=[RuntimeError, ValueError], ) - - # Configure the step + step_config = StepConfig( retry_strategy=create_retry_strategy(retry_config), step_semantics=StepSemantics.AT_LEAST_ONCE_PER_RETRY, ) - - # Use the configuration + result = context.step(process_data(event["data"]), config=step_config) return result diff --git a/examples/python/operations/steps/step-signature.py b/examples/python/operations/steps/step-signature.py new file mode 100644 index 0000000..aef498b --- /dev/null +++ b/examples/python/operations/steps/step-signature.py @@ -0,0 +1,9 @@ +from aws_durable_execution_sdk_python import StepContext +from aws_durable_execution_sdk_python.config import StepConfig + + +def step( + func: Callable[[StepContext], T], + name: str | None = None, + config: StepConfig | None = None, +) -> T: ... diff --git a/examples/python/core/steps/validate-order.py b/examples/python/operations/steps/validate-order.py similarity index 51% rename from examples/python/core/steps/validate-order.py rename to examples/python/operations/steps/validate-order.py index 0ff4c3f..573a6a3 100644 --- a/examples/python/core/steps/validate-order.py +++ b/examples/python/operations/steps/validate-order.py @@ -1,11 +1,21 @@ -from aws_durable_execution_sdk_python import durable_step, StepContext +from aws_durable_execution_sdk_python import ( + DurableContext, + StepContext, + durable_execution, + durable_step, +) +from aws_durable_execution_sdk_python.config import StepConfig +from aws_durable_execution_sdk_python.retries import ( + RetryStrategyConfig, + create_retry_strategy, +) + @durable_step def validate_order(step_context: StepContext, order_id: str) -> dict: - """Validate an order.""" - # Your validation logic here return {"order_id": order_id, "valid": True} + @durable_execution def handler(event: dict, context: DurableContext) -> dict: order_id = event["order_id"] diff --git a/examples/python/core/steps/unreliable-operation.py b/examples/python/sdk-reference/error-handling/unreliable-operation.py similarity index 82% rename from examples/python/core/steps/unreliable-operation.py rename to examples/python/sdk-reference/error-handling/unreliable-operation.py index 68e1fa6..561e264 100644 --- a/examples/python/core/steps/unreliable-operation.py +++ b/examples/python/sdk-reference/error-handling/unreliable-operation.py @@ -1,9 +1,10 @@ from random import random + from aws_durable_execution_sdk_python import ( DurableContext, + StepContext, durable_execution, durable_step, - StepContext, ) from aws_durable_execution_sdk_python.config import StepConfig from aws_durable_execution_sdk_python.retries import ( @@ -11,24 +12,24 @@ create_retry_strategy, ) + @durable_step def unreliable_operation(step_context: StepContext) -> str: - """Operation that might fail.""" if random() > 0.5: raise RuntimeError("Random error occurred") return "Operation succeeded" + @durable_execution def handler(event: dict, context: DurableContext) -> str: - # Only retry RuntimeError, not other exceptions retry_config = RetryStrategyConfig( max_attempts=3, retryable_error_types=[RuntimeError], ) - + result = context.step( unreliable_operation(), - config=StepConfig(create_retry_strategy(retry_config)), + config=StepConfig(retry_strategy=create_retry_strategy(retry_config)), ) - + return result diff --git a/examples/python/core/steps/basic-step-testing.py b/examples/python/testing-patterns/basic-tests/basic-step-testing.py similarity index 61% rename from examples/python/core/steps/basic-step-testing.py rename to examples/python/testing-patterns/basic-tests/basic-step-testing.py index e91399c..b3ea2ed 100644 --- a/examples/python/core/steps/basic-step-testing.py +++ b/examples/python/testing-patterns/basic-tests/basic-step-testing.py @@ -1,18 +1,16 @@ import pytest -from aws_durable_execution_sdk_python_testing import InvocationStatus +from aws_durable_execution_sdk_python.execution import InvocationStatus + from my_function import handler + @pytest.mark.durable_execution( handler=handler, lambda_function_name="my_function", ) def test_step(durable_runner): - """Test a function with steps.""" with durable_runner: result = durable_runner.run(input={"data": "test"}, timeout=10) - - # Check overall status + assert result.status is InvocationStatus.SUCCEEDED - - # Check final result - assert result.result == 8 + assert result.result is not None diff --git a/examples/python/core/steps/inspect-step-results.py b/examples/python/testing-patterns/basic-tests/inspect-step-results.py similarity index 71% rename from examples/python/core/steps/inspect-step-results.py rename to examples/python/testing-patterns/basic-tests/inspect-step-results.py index 0b76f48..87e87db 100644 --- a/examples/python/core/steps/inspect-step-results.py +++ b/examples/python/testing-patterns/basic-tests/inspect-step-results.py @@ -1,15 +1,16 @@ +import pytest +from aws_durable_execution_sdk_python.execution import InvocationStatus + +from my_function import handler + + @pytest.mark.durable_execution( handler=handler, lambda_function_name="my_function", ) def test_step_result(durable_runner): - """Test and inspect step results.""" with durable_runner: result = durable_runner.run(input={"data": "test"}, timeout=10) - - # Get step by name + step_result = result.get_step("add_numbers") - assert step_result.result == 8 - - # Check step status assert step_result.status is InvocationStatus.SUCCEEDED diff --git a/examples/python/core/steps/test-error-handling.py b/examples/python/testing-patterns/basic-tests/test-error-handling.py similarity index 62% rename from examples/python/core/steps/test-error-handling.py rename to examples/python/testing-patterns/basic-tests/test-error-handling.py index 743b2c2..4a69017 100644 --- a/examples/python/core/steps/test-error-handling.py +++ b/examples/python/testing-patterns/basic-tests/test-error-handling.py @@ -1,14 +1,16 @@ +import pytest +from aws_durable_execution_sdk_python.execution import InvocationStatus + +from my_function import handler_with_error + + @pytest.mark.durable_execution( handler=handler_with_error, lambda_function_name="error_function", ) def test_step_error(durable_runner): - """Test step error handling.""" with durable_runner: result = durable_runner.run(input={}, timeout=10) - - # Function should fail + assert result.status is InvocationStatus.FAILED - - # Check the error - assert "RuntimeError" in str(result.error) + assert result.error is not None diff --git a/examples/python/core/steps/test-retry-behavior.py b/examples/python/testing-patterns/basic-tests/test-retry-behavior.py similarity index 74% rename from examples/python/core/steps/test-retry-behavior.py rename to examples/python/testing-patterns/basic-tests/test-retry-behavior.py index 4aacf43..3d67ebc 100644 --- a/examples/python/core/steps/test-retry-behavior.py +++ b/examples/python/testing-patterns/basic-tests/test-retry-behavior.py @@ -1,15 +1,18 @@ +import pytest +from aws_durable_execution_sdk_python.execution import InvocationStatus + +from my_function import handler_with_retry + + @pytest.mark.durable_execution( handler=handler_with_retry, lambda_function_name="retry_function", ) def test_step_retry(durable_runner): - """Test step retry behavior.""" with durable_runner: result = durable_runner.run(input={}, timeout=30) - - # Function should eventually succeed after retries + assert result.status is InvocationStatus.SUCCEEDED - - # Inspect the step that retried + step_result = result.get_step("unreliable_operation") assert step_result.status is InvocationStatus.SUCCEEDED diff --git a/examples/typescript/core/steps/add-numbers.ts b/examples/typescript/core/steps/add-numbers.ts deleted file mode 100644 index 48cdd22..0000000 --- a/examples/typescript/core/steps/add-numbers.ts +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/typescript/core/steps/basic-step-testing.ts b/examples/typescript/core/steps/basic-step-testing.ts deleted file mode 100644 index 48cdd22..0000000 --- a/examples/typescript/core/steps/basic-step-testing.ts +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/typescript/core/steps/charge-credit-card.ts b/examples/typescript/core/steps/charge-credit-card.ts deleted file mode 100644 index 48cdd22..0000000 --- a/examples/typescript/core/steps/charge-credit-card.ts +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/typescript/core/steps/explicit-step-name.ts b/examples/typescript/core/steps/explicit-step-name.ts deleted file mode 100644 index 48cdd22..0000000 --- a/examples/typescript/core/steps/explicit-step-name.ts +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/typescript/core/steps/exponential-backoff.ts b/examples/typescript/core/steps/exponential-backoff.ts deleted file mode 100644 index 48cdd22..0000000 --- a/examples/typescript/core/steps/exponential-backoff.ts +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/typescript/core/steps/fetch-user.ts b/examples/typescript/core/steps/fetch-user.ts deleted file mode 100644 index 48cdd22..0000000 --- a/examples/typescript/core/steps/fetch-user.ts +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/typescript/core/steps/inspect-step-results.ts b/examples/typescript/core/steps/inspect-step-results.ts deleted file mode 100644 index 48cdd22..0000000 --- a/examples/typescript/core/steps/inspect-step-results.ts +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/typescript/core/steps/lambda-step-example.ts b/examples/typescript/core/steps/lambda-step-example.ts index 48cdd22..3ef7184 100644 --- a/examples/typescript/core/steps/lambda-step-example.ts +++ b/examples/typescript/core/steps/lambda-step-example.ts @@ -1 +1,9 @@ -// Coming soon... +import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js"; + +export const handler = withDurableExecution( + async (event: any, context: DurableContext) => { + // Lambda without a name + const result = await context.step(async () => "some value"); + return result; + }, +); diff --git a/examples/typescript/core/steps/lambda-step-no-name.ts b/examples/typescript/core/steps/lambda-step-no-name.ts deleted file mode 100644 index 48cdd22..0000000 --- a/examples/typescript/core/steps/lambda-step-no-name.ts +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/typescript/core/steps/multi-argument-step.ts b/examples/typescript/core/steps/multi-argument-step.ts deleted file mode 100644 index 48cdd22..0000000 --- a/examples/typescript/core/steps/multi-argument-step.ts +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/typescript/core/steps/process-data.ts b/examples/typescript/core/steps/process-data.ts deleted file mode 100644 index 48cdd22..0000000 --- a/examples/typescript/core/steps/process-data.ts +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/typescript/core/steps/process-payment.ts b/examples/typescript/core/steps/process-payment.ts deleted file mode 100644 index 48cdd22..0000000 --- a/examples/typescript/core/steps/process-payment.ts +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/typescript/core/steps/step-signature.ts b/examples/typescript/core/steps/step-signature.ts deleted file mode 100644 index 48cdd22..0000000 --- a/examples/typescript/core/steps/step-signature.ts +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/typescript/core/steps/test-error-handling.ts b/examples/typescript/core/steps/test-error-handling.ts deleted file mode 100644 index 48cdd22..0000000 --- a/examples/typescript/core/steps/test-error-handling.ts +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/typescript/core/steps/test-retry-behavior.ts b/examples/typescript/core/steps/test-retry-behavior.ts deleted file mode 100644 index 48cdd22..0000000 --- a/examples/typescript/core/steps/test-retry-behavior.ts +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/typescript/core/steps/unreliable-operation.ts b/examples/typescript/core/steps/unreliable-operation.ts deleted file mode 100644 index 48cdd22..0000000 --- a/examples/typescript/core/steps/unreliable-operation.ts +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/typescript/core/steps/validate-order.ts b/examples/typescript/core/steps/validate-order.ts deleted file mode 100644 index 48cdd22..0000000 --- a/examples/typescript/core/steps/validate-order.ts +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/typescript/operations/steps/add-numbers.ts b/examples/typescript/operations/steps/add-numbers.ts new file mode 100644 index 0000000..b4c6caf --- /dev/null +++ b/examples/typescript/operations/steps/add-numbers.ts @@ -0,0 +1,16 @@ +import { + DurableContext, + StepContext, + withDurableExecution, +} from "@aws/durable-execution-sdk-js"; + +async function addNumbers(ctx: StepContext, a: number, b: number): Promise { + return a + b; +} + +export const handler = withDurableExecution( + async (event: any, context: DurableContext) => { + const result = await context.step("add_numbers", (ctx) => addNumbers(ctx, 5, 3)); + return result; + }, +); diff --git a/examples/typescript/operations/steps/lambda-step-no-name.ts b/examples/typescript/operations/steps/lambda-step-no-name.ts new file mode 100644 index 0000000..92d0dc0 --- /dev/null +++ b/examples/typescript/operations/steps/lambda-step-no-name.ts @@ -0,0 +1,9 @@ +import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js"; + +export const handler = withDurableExecution( + async (event: any, context: DurableContext) => { + // Lambda without a name — no automatic name + const result = await context.step(async () => "some value"); + return result; + }, +); diff --git a/examples/typescript/operations/steps/multi-argument-step.ts b/examples/typescript/operations/steps/multi-argument-step.ts new file mode 100644 index 0000000..b813f0e --- /dev/null +++ b/examples/typescript/operations/steps/multi-argument-step.ts @@ -0,0 +1,12 @@ +import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js"; + +async function myStep(arg1: string, arg2: number): Promise { + return `${arg1}: ${arg2}`; +} + +export const handler = withDurableExecution( + async (event: any, context: DurableContext) => { + const result = await context.step("my_step", async () => myStep("value", 42)); + return result; + }, +); diff --git a/examples/typescript/operations/steps/passing-data-correct.ts b/examples/typescript/operations/steps/passing-data-correct.ts new file mode 100644 index 0000000..84ae121 --- /dev/null +++ b/examples/typescript/operations/steps/passing-data-correct.ts @@ -0,0 +1,24 @@ +import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js"; + +async function registerUser(email: string): Promise { + return `user-${email}`; +} + +async function sendFollowUpEmail(userId: string): Promise { + // send email to user +} + +// ✅ CORRECT: userId is returned from the step and restored from checkpoint on replay +export const handler = withDurableExecution( + async (event: { email: string }, context: DurableContext) => { + const userId = await context.step("register-user", async () => { + return await registerUser(event.email); + }); + + await context.wait("follow-up-delay", { minutes: 10 }); + + await context.step("send-follow-up-email", async () => { + await sendFollowUpEmail(userId); // userId restored from checkpoint + }); + }, +); diff --git a/examples/typescript/operations/steps/passing-data-wrong.ts b/examples/typescript/operations/steps/passing-data-wrong.ts new file mode 100644 index 0000000..532ab4e --- /dev/null +++ b/examples/typescript/operations/steps/passing-data-wrong.ts @@ -0,0 +1,26 @@ +import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js"; + +async function registerUser(email: string): Promise { + return `user-${email}`; +} + +async function sendFollowUpEmail(userId: string): Promise { + // send email to user +} + +// ❌ WRONG: userId mutation is lost on replay after the wait +export const handler = withDurableExecution( + async (event: { email: string }, context: DurableContext) => { + let userId = ""; + + await context.step("register-user", async () => { + userId = await registerUser(event.email); // ⚠️ Lost on replay! + }); + + await context.wait("follow-up-delay", { minutes: 10 }); + + await context.step("send-follow-up-email", async () => { + await sendFollowUpEmail(userId); // userId is "" on replay + }); + }, +); diff --git a/examples/typescript/operations/steps/process-data.ts b/examples/typescript/operations/steps/process-data.ts new file mode 100644 index 0000000..84157c4 --- /dev/null +++ b/examples/typescript/operations/steps/process-data.ts @@ -0,0 +1,29 @@ +import { + DurableContext, + StepConfig, + StepSemantics, + createRetryStrategy, + withDurableExecution, +} from "@aws/durable-execution-sdk-js"; + +async function processData(data: string): Promise { + return { processed: data, status: "completed" }; +} + +const stepConfig: StepConfig = { + retryStrategy: createRetryStrategy({ + maxAttempts: 3, + }), + semantics: StepSemantics.AtLeastOncePerRetry, +}; + +export const handler = withDurableExecution( + async (event: any, context: DurableContext) => { + const result = await context.step( + "process_data", + async () => processData(event.data), + stepConfig, + ); + return result; + }, +); diff --git a/examples/typescript/operations/steps/process-payment.ts b/examples/typescript/operations/steps/process-payment.ts new file mode 100644 index 0000000..0c8e034 --- /dev/null +++ b/examples/typescript/operations/steps/process-payment.ts @@ -0,0 +1,19 @@ +import { + DurableContext, + StepContext, + withDurableExecution, +} from "@aws/durable-execution-sdk-js"; + +async function processPayment(ctx: StepContext, amount: number): Promise { + return { status: "completed", amount }; +} + +export const handler = withDurableExecution( + async (event: any, context: DurableContext) => { + // Step is automatically named "processPayment" + const result = await context.step("process_payment", (ctx) => + processPayment(ctx, 100.0), + ); + return result; + }, +); diff --git a/examples/typescript/operations/steps/step-signature.ts b/examples/typescript/operations/steps/step-signature.ts new file mode 100644 index 0000000..eba2c51 --- /dev/null +++ b/examples/typescript/operations/steps/step-signature.ts @@ -0,0 +1,7 @@ +import { DurableContext, StepConfig, StepFunc } from "@aws/durable-execution-sdk-js"; + +// Sync signature (unnamed) +context.step(fn: StepFunc, config?: StepConfig): DurablePromise + +// Named signature +context.step(name: string | undefined, fn: StepFunc, config?: StepConfig): DurablePromise diff --git a/examples/typescript/operations/steps/validate-order.ts b/examples/typescript/operations/steps/validate-order.ts new file mode 100644 index 0000000..281fcac --- /dev/null +++ b/examples/typescript/operations/steps/validate-order.ts @@ -0,0 +1,19 @@ +import { + DurableContext, + StepContext, + withDurableExecution, +} from "@aws/durable-execution-sdk-js"; + +async function validateOrder(ctx: StepContext, orderId: string): Promise { + return { order_id: orderId, valid: true }; +} + +export const handler = withDurableExecution( + async (event: any, context: DurableContext) => { + const orderId = event.order_id; + const validation = await context.step("validate_order", (ctx) => + validateOrder(ctx, orderId), + ); + return validation; + }, +); diff --git a/examples/typescript/sdk-reference/error-handling/unreliable-operation.ts b/examples/typescript/sdk-reference/error-handling/unreliable-operation.ts new file mode 100644 index 0000000..c694f01 --- /dev/null +++ b/examples/typescript/sdk-reference/error-handling/unreliable-operation.ts @@ -0,0 +1,31 @@ +import { + DurableContext, + StepConfig, + createRetryStrategy, + withDurableExecution, +} from "@aws/durable-execution-sdk-js"; + +class TransientError extends Error {} + +const stepConfig: StepConfig = { + retryStrategy: createRetryStrategy({ + maxAttempts: 3, + retryableErrorTypes: [TransientError], + }), +}; + +async function unreliableOperation(): Promise { + if (Math.random() > 0.5) throw new TransientError("Random error occurred"); + return "Operation succeeded"; +} + +export const handler = withDurableExecution( + async (event: any, context: DurableContext) => { + const result = await context.step( + "unreliable_operation", + async () => unreliableOperation(), + stepConfig, + ); + return result; + }, +); diff --git a/examples/typescript/testing-patterns/basic-tests/basic-step-testing.ts b/examples/typescript/testing-patterns/basic-tests/basic-step-testing.ts new file mode 100644 index 0000000..6b35be4 --- /dev/null +++ b/examples/typescript/testing-patterns/basic-tests/basic-step-testing.ts @@ -0,0 +1,27 @@ +import { + LocalDurableTestRunner, + OperationStatus, +} from "@aws/durable-execution-sdk-js-testing"; +import { ExecutionStatus } from "@aws-sdk/client-lambda"; +import { handler } from "./add-numbers"; + +let runner: LocalDurableTestRunner; + +beforeAll(async () => { + await LocalDurableTestRunner.setupTestEnvironment({ skipTime: true }); +}); + +afterAll(async () => { + await LocalDurableTestRunner.teardownTestEnvironment(); +}); + +beforeEach(() => { + runner = new LocalDurableTestRunner({ handlerFunction: handler }); +}); + +it("should execute a step and return the result", async () => { + const result = await runner.run(); + + expect(result.getStatus()).toBe(ExecutionStatus.SUCCEEDED); + expect(result.getResult()).toBe(8); +}); diff --git a/examples/typescript/testing-patterns/basic-tests/inspect-step-results.ts b/examples/typescript/testing-patterns/basic-tests/inspect-step-results.ts new file mode 100644 index 0000000..9ab4964 --- /dev/null +++ b/examples/typescript/testing-patterns/basic-tests/inspect-step-results.ts @@ -0,0 +1,27 @@ +import { + LocalDurableTestRunner, + OperationStatus, +} from "@aws/durable-execution-sdk-js-testing"; +import { ExecutionStatus } from "@aws-sdk/client-lambda"; +import { handler } from "./handler-with-error"; + +let runner: LocalDurableTestRunner; + +beforeAll(async () => { + await LocalDurableTestRunner.setupTestEnvironment({ skipTime: true }); +}); + +afterAll(async () => { + await LocalDurableTestRunner.teardownTestEnvironment(); +}); + +beforeEach(() => { + runner = new LocalDurableTestRunner({ handlerFunction: handler }); +}); + +it("should fail when step throws an unretried error", async () => { + const result = await runner.run(); + + expect(result.getStatus()).toBe(ExecutionStatus.FAILED); + expect(result.getError().errorMessage).toBeDefined(); +}); diff --git a/examples/typescript/testing-patterns/basic-tests/test-error-handling.ts b/examples/typescript/testing-patterns/basic-tests/test-error-handling.ts new file mode 100644 index 0000000..b713f4a --- /dev/null +++ b/examples/typescript/testing-patterns/basic-tests/test-error-handling.ts @@ -0,0 +1,33 @@ +import { + LocalDurableTestRunner, + OperationStatus, + OperationType, +} from "@aws/durable-execution-sdk-js-testing"; +import { ExecutionStatus } from "@aws-sdk/client-lambda"; +import { handler } from "./add-numbers"; + +let runner: LocalDurableTestRunner; + +beforeAll(async () => { + await LocalDurableTestRunner.setupTestEnvironment({ skipTime: true }); +}); + +afterAll(async () => { + await LocalDurableTestRunner.teardownTestEnvironment(); +}); + +beforeEach(() => { + runner = new LocalDurableTestRunner({ handlerFunction: handler }); +}); + +it("should inspect step result by name", async () => { + const stepOp = runner.getOperation("add_numbers"); + + const result = await runner.run(); + + expect(result.getStatus()).toBe(ExecutionStatus.SUCCEEDED); + + const details = stepOp.getStepDetails(); + expect(details?.result).toBeDefined(); + expect(stepOp.getStatus()).toBe(OperationStatus.SUCCEEDED); +}); diff --git a/examples/typescript/testing-patterns/basic-tests/test-retry-behavior.ts b/examples/typescript/testing-patterns/basic-tests/test-retry-behavior.ts new file mode 100644 index 0000000..8cfbbdc --- /dev/null +++ b/examples/typescript/testing-patterns/basic-tests/test-retry-behavior.ts @@ -0,0 +1,29 @@ +import { + LocalDurableTestRunner, + OperationStatus, +} from "@aws/durable-execution-sdk-js-testing"; +import { ExecutionStatus } from "@aws-sdk/client-lambda"; +import { handler } from "./unreliable-operation"; + +let runner: LocalDurableTestRunner; + +beforeAll(async () => { + await LocalDurableTestRunner.setupTestEnvironment({ skipTime: true }); +}); + +afterAll(async () => { + await LocalDurableTestRunner.teardownTestEnvironment(); +}); + +beforeEach(() => { + runner = new LocalDurableTestRunner({ handlerFunction: handler }); +}); + +it("should retry and eventually succeed", async () => { + const result = await runner.run(); + + expect(result.getStatus()).toBe(ExecutionStatus.SUCCEEDED); + + const stepOp = runner.getOperation("unreliable_operation"); + expect(stepOp.getStatus()).toBe(OperationStatus.SUCCEEDED); +});