diff --git a/.github/workflows/notify_slack.yml b/.github/workflows/notify_slack.yml index d45ccd4..8ddb5d0 100644 --- a/.github/workflows/notify_slack.yml +++ b/.github/workflows/notify_slack.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Send issue notification to Slack if: github.event_name == 'issues' - uses: slackapi/slack-github-action@v3.0.1 + uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1 with: webhook: ${{ secrets.SLACK_WEBHOOK_URL_ISSUE }} webhook-type: incoming-webhook @@ -27,7 +27,7 @@ jobs: - name: Send pull request notification to Slack if: github.event_name == 'pull_request_target' - uses: slackapi/slack-github-action@v3.0.1 + uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1 with: webhook: ${{ secrets.SLACK_WEBHOOK_URL_PR }} webhook-type: incoming-webhook diff --git a/README.md b/README.md index f6c7822..50086a3 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ for details. - [AWS Lambda durable functions Documentation](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html) - [JavaScript SDK Repository](https://github.com/aws/aws-durable-execution-sdk-js) - [Python SDK Repository](https://github.com/aws/aws-durable-execution-sdk-python) +- [Java SDK Repository](https://github.com/aws/aws-durable-execution-sdk-java) ## Feedback & Support diff --git a/aws-lambda-durable-functions-power/POWER.md b/aws-lambda-durable-functions-power/POWER.md index 578e286..ca7a587 100644 --- a/aws-lambda-durable-functions-power/POWER.md +++ b/aws-lambda-durable-functions-power/POWER.md @@ -31,6 +31,7 @@ Before using AWS Lambda durable functions, verify: 2. **Runtime environment** is ready: - For TypeScript/JavaScript: Node.js 22+ (`node --version`) - For Python: Python 3.11+ (`python --version`. Note that currently only Lambda runtime environments 3.13+ come with the Durable Execution SDK pre-installed. 3.11 is the min supported Python version by the Durable SDK itself, however, you could use OCI to bring your own container image with your own Python runtime + Durable SDK.) + - For Java: Java 17+ (`java --version`) 3. **Deployment capability** exists (one of): - AWS SAM CLI (`sam --version`) 1.153.1 or higher @@ -40,7 +41,8 @@ Before using AWS Lambda durable functions, verify: ## Step 2: Check user and project preferences Ask which IaC framework to use for new projects. -Ask which programming language to use if unclear, clarify between JavaScript and TypeScript if necessary. +Ask which programming language to use. Supported languages are: **TypeScript**, **JavaScript**, **Python**, and **Java**. If the user says JavaScript or TypeScript, clarify between the two if necessary. +**IMPORTANT**: Only install the SDK and set up the project for the language the user chose. Do NOT install SDKs or create project files for other languages. Ask to create a git repo for projects if one doesn't exist already. ### Error Scenarios @@ -59,6 +61,8 @@ Ask to create a git repo for projects if one doesn't exist already. ### Step 3: Install SDK +Install **only** the SDK for the language the user selected in Step 2. + **For TypeScript/JavaScript:** ```bash @@ -73,6 +77,24 @@ pip install aws-durable-execution-sdk-python pip install aws-durable-execution-sdk-python-testing ``` +**For Java (Maven):** + +```xml + + software.amazon.lambda.durable + aws-durable-execution-sdk-java + 1.0.0 + + + + + software.amazon.lambda.durable + aws-durable-execution-sdk-java-testing + 1.0.0 + test + +``` + ## When to Load Reference Files Load the appropriate reference file based on what the user is working on: @@ -115,12 +137,25 @@ def handler(event: dict, context: DurableContext) -> dict: return result ``` +**Java:** + +```java +public class MyHandler extends DurableHandler { + @Override + public MyOutput handleRequest(MyInput input, DurableContext ctx) { + var result = ctx.step("process", Result.class, + stepCtx -> processData(input)); + return new MyOutput(result); + } +} +``` + ### Critical Rules -1. **All non-deterministic code MUST be in steps** (Date.now, Math.random, API calls) +1. **All non-deterministic code MUST be in steps** (Date.now, Math.random, UUID.randomUUID, API calls) 2. **Cannot nest durable operations** - use `runInChildContext` to group operations 3. **Closure mutations are lost on replay** - return values from steps -4. **Side effects outside steps repeat** - use `context.logger` (replay-aware) +4. **Side effects outside steps repeat** - use `context.logger` / `ctx.getLogger()` (replay-aware) ### Python API Differences @@ -131,6 +166,22 @@ The Python SDK differs from TypeScript in several key areas: - **Exceptions**: `ExecutionError` (permanent), `InvocationError` (transient), `CallbackError` (callback failures) - **Testing**: Use `DurableFunctionTestRunner` class directly - instantiate with handler, use context manager, call `run(input=...)` +### Java API Differences + +The Java SDK differs from TypeScript/Python in several key areas: + +- **Handler**: Extend `DurableHandler` and implement `handleRequest(I input, DurableContext ctx)` +- **Steps**: `ctx.step("name", ResultType.class, stepCtx -> ...)` — type class required for deserialization +- **Generic types**: Use `TypeToken` for parameterized types: `ctx.step("name", new TypeToken>() {}, stepCtx -> ...)` +- **Wait**: `ctx.wait("name", Duration.ofMinutes(5))` — uses `java.time.Duration` +- **Async**: `stepAsync()`, `waitAsync()`, `mapAsync()`, `runInChildContextAsync()` return `DurableFuture` +- **Callbacks**: `ctx.createCallback("name", Type.class)` returns `DurableCallbackFuture`; or use `ctx.waitForCallback()` +- **Map**: `ctx.map("name", items, Type.class, (item, index, childCtx) -> ...)` with `MapFunction` interface +- **Configuration**: Override `createConfiguration()` to return `DurableConfig` for custom SerDes, thread pools, Lambda client +- **Exceptions**: `StepFailedException`, `StepInterruptedException`, `CallbackTimeoutException`, `CallbackFailedException`, `WaitForConditionFailedException` +- **Testing**: `LocalDurableTestRunner.create(InputType.class, handler)` with `runUntilComplete(input)` and `getOperation("name")` +- **Logging**: `ctx.getLogger()` returns `DurableLogger` (SLF4J MDC-based, replay-aware) + ### Invocation Requirements Durable functions **require qualified ARNs** (version, alias, or `$LATEST`): @@ -163,10 +214,10 @@ See here: https://docs.aws.amazon.com/lambda/latest/dg/durable-security.html When writing or reviewing durable function code, ALWAYS check for these replay model violations: -1. **Non-deterministic code outside steps**: `Date.now()`, `Math.random()`, UUID generation, API calls, database queries must all be inside steps +1. **Non-deterministic code outside steps**: `Date.now()`, `Math.random()`, `UUID.randomUUID()`, API calls, database queries must all be inside steps 2. **Nested durable operations in step functions**: Cannot call `context.step()`, `context.wait()`, or `context.invoke()` inside a step function — use `context.runInChildContext()` instead 3. **Closure mutations that won't persist**: Variables mutated inside steps are NOT preserved across replays — return values from steps instead -4. **Side effects outside steps that repeat on replay**: Use `context.logger` for logging (it is replay-aware and deduplicates automatically) +4. **Side effects outside steps that repeat on replay**: Use `context.logger` / `ctx.getLogger()` for logging (it is replay-aware and deduplicates automatically) When implementing or modifying tests for durable functions, ALWAYS verify: @@ -175,10 +226,12 @@ When implementing or modifying tests for durable functions, ALWAYS verify: 3. Replay behavior is tested with multiple invocations 4. TypeScript: Use `LocalDurableTestRunner` for local testing 5. Python: Use `DurableFunctionTestRunner` class directly +6. Java: Use `LocalDurableTestRunner.create(InputType.class, handler)` with `runUntilComplete(input)` ## Resources - [AWS Lambda durable functions Documentation](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html) - [JavaScript SDK Repository](https://github.com/aws/aws-durable-execution-sdk-js) - [Python SDK Repository](https://github.com/aws/aws-durable-execution-sdk-python) +- [Java SDK Repository](https://github.com/aws/aws-durable-execution-sdk-java) - [IAM Policy Reference](https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AWSLambdaBasicDurableExecutionRolePolicy.html) diff --git a/aws-lambda-durable-functions-power/README.md b/aws-lambda-durable-functions-power/README.md index d634613..956ad99 100644 --- a/aws-lambda-durable-functions-power/README.md +++ b/aws-lambda-durable-functions-power/README.md @@ -79,6 +79,7 @@ When you mention these keywords, Kiro will automatically load this power: - [AWS Lambda durable functions Documentation](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html) - [JavaScript SDK Repository](https://github.com/aws/aws-durable-execution-sdk-js) - [Python SDK Repository](https://github.com/aws/aws-durable-execution-sdk-python) +- [Java SDK Repository](https://github.com/aws/aws-durable-execution-sdk-java) - [Kiro Powers Documentation](https://kiro.dev/docs/powers/create/) ## License diff --git a/aws-lambda-durable-functions-power/steering/advanced-patterns.md b/aws-lambda-durable-functions-power/steering/advanced-patterns.md index c0983ee..d3953f5 100644 --- a/aws-lambda-durable-functions-power/steering/advanced-patterns.md +++ b/aws-lambda-durable-functions-power/steering/advanced-patterns.md @@ -96,6 +96,32 @@ def handler(event: dict, context: DurableContext) -> str: context.logger.debug('Tool result added', extra={'tool': tool['name']}) ``` +**Java:** + +```java +public class AIAgentHandler extends DurableHandler { + @Override + public String handleRequest(AgentInput input, DurableContext ctx) { + ctx.getLogger().info("Starting AI agent: {}", input.getPrompt()); + var messages = new ArrayList>(); + messages.add(Map.of("role", "user", "content", input.getPrompt())); + + while (true) { + var result = ctx.step("invoke-model", AIResponse.class, + stepCtx -> invokeAIModel(messages)); + + if (result.getTool() == null) return result.getResponse(); + + var toolResult = ctx.step("execute-tool-" + result.getTool().getName(), + String.class, + stepCtx -> executeTool(result.getTool(), result.getResponse())); + + messages.add(Map.of("role", "assistant", "content", toolResult)); + } + } +} +``` + ## Step Semantics Deep Dive ### AtMostOncePerRetry vs AtLeastOncePerRetry @@ -267,6 +293,29 @@ const result = await context.step( console.log(result.createdAt instanceof Date); // true ``` +### Java Custom SerDes + +```java +// Java uses Jackson by default — POJOs serialize automatically +// For custom serialization, implement the SerDes interface: +public interface SerDes { + String serialize(Object value); + T deserialize(String data, Class type); + T deserialize(String data, TypeToken typeToken); +} + +// Per-step custom SerDes +var result = ctx.step("fetch-data", ComplexType.class, + stepCtx -> fetchComplexData(), + StepConfig.builder().serDes(new MyCustomSerDes()).build()); + +// Global custom SerDes via DurableConfig +@Override +protected DurableConfig createConfiguration() { + return DurableConfig.builder().withSerDes(new MyCustomSerDes()).build(); +} +``` + ### Complex Object Graphs **TypeScript:** diff --git a/aws-lambda-durable-functions-power/steering/concurrent-operations.md b/aws-lambda-durable-functions-power/steering/concurrent-operations.md index 4df17b9..e309aa3 100644 --- a/aws-lambda-durable-functions-power/steering/concurrent-operations.md +++ b/aws-lambda-durable-functions-power/steering/concurrent-operations.md @@ -60,6 +60,24 @@ results.throw_if_error() all_results = results.get_results() ``` +**Java:** + +```java +var items = List.of("order-1", "order-2", "order-3", "order-4", "order-5"); +var result = ctx.map("process-items", items, OrderResult.class, + (item, index, childCtx) -> { + return childCtx.step("process-" + index, OrderResult.class, + stepCtx -> orderService.process(item)); + }, + MapConfig.builder() + .maxConcurrency(3) + .completionConfig(CompletionConfig.minSuccessful(4)) + .build()); + +assertTrue(result.allSucceeded()); +var allResults = result.results(); +``` + ## Parallel Operations Run heterogeneous operations concurrently: @@ -113,6 +131,22 @@ results = context.parallel( user, orders, preferences = results.get_results() ``` +**Java (using async child contexts):** + +```java +var futureA = ctx.runInChildContextAsync("fetch-user", User.class, + child -> child.step("fetch", User.class, stepCtx -> fetchUser(userId))); +var futureB = ctx.runInChildContextAsync("fetch-orders", OrderList.class, + child -> child.step("fetch", OrderList.class, stepCtx -> fetchOrders(userId))); +var futureC = ctx.runInChildContextAsync("fetch-prefs", Prefs.class, + child -> child.step("fetch", Prefs.class, stepCtx -> fetchPreferences(userId))); + +var results = DurableFuture.allOf(futureA, futureB, futureC); +User user = futureA.get(); +OrderList orders = futureB.get(); +Prefs prefs = futureC.get(); +``` + ## Completion Policies ### Minimum Successful diff --git a/aws-lambda-durable-functions-power/steering/deployment-iac.md b/aws-lambda-durable-functions-power/steering/deployment-iac.md index cf2c0f7..b8e4e30 100644 --- a/aws-lambda-durable-functions-power/steering/deployment-iac.md +++ b/aws-lambda-durable-functions-power/steering/deployment-iac.md @@ -34,7 +34,7 @@ Resources: Type: AWS::Lambda::Function Properties: FunctionName: myDurableFunction - Runtime: nodejs24.x # or python3.14 + Runtime: nodejs24.x # or python3.14 (see Java SAM section below for Java) Handler: index.handler Role: !GetAtt DurableFunctionRole.Arn Code: @@ -89,7 +89,7 @@ export class DurableFunctionStack extends cdk.Stack { super(scope, id, props); const durableFunction = new lambda.Function(this, 'DurableFunction', { - runtime: lambda.Runtime.NODEJS_24_X, // or PYTHON_3_14 + runtime: lambda.Runtime.NODEJS_24_X, // or PYTHON_3_14 (see Java SAM section for Java) handler: 'index.handler', code: lambda.Code.fromAsset('lambda'), durableConfig: { diff --git a/aws-lambda-durable-functions-power/steering/error-handling.md b/aws-lambda-durable-functions-power/steering/error-handling.md index c1c0cbd..51e675d 100644 --- a/aws-lambda-durable-functions-power/steering/error-handling.md +++ b/aws-lambda-durable-functions-power/steering/error-handling.md @@ -55,6 +55,17 @@ result = context.step( ) ``` +**Java:** + +```java +var result = ctx.step("api-call", Response.class, + stepCtx -> callAPI(), + StepConfig.builder() + .retryStrategy(RetryStrategies.exponentialBackoff( + 5, Duration.ofSeconds(1), Duration.ofSeconds(60), 2.0, JitterStrategy.FULL)) + .build()); +``` + ## Custom Retry Logic **TypeScript:** @@ -100,6 +111,20 @@ def custom_retry(error: Exception, attempt: int) -> RetryDecision: return RetryDecision(should_retry=False) ``` +**Java:** + +```java +var result = ctx.step("custom-retry", Response.class, + stepCtx -> riskyOperation(), + StepConfig.builder() + .retryStrategy((error, attemptCount) -> { + if (error instanceof ClientException) return RetryDecision.noRetry(); + if (attemptCount < 5) return RetryDecision.retryAfter(Duration.ofSeconds((long) Math.pow(2, attemptCount))); + return RetryDecision.noRetry(); + }) + .build()); +``` + ## Error Classification ### Retryable vs Non-Retryable @@ -232,6 +257,41 @@ def handler(event: dict, context: DurableContext) -> dict: raise error ``` +**Java:** + +```java +public class OrderSaga extends DurableHandler { + @Override + public OrderResult handleRequest(OrderInput input, DurableContext ctx) { + var compensations = new ArrayList>(); + try { + var reservation = ctx.step("reserve-inventory", Reservation.class, + stepCtx -> inventoryService.reserve(input.getItems())); + compensations.add(Map.entry("cancel-reservation", + () -> inventoryService.cancelReservation(reservation.getId()))); + + var payment = ctx.step("charge-payment", Payment.class, + stepCtx -> paymentService.charge(input.getPaymentMethod(), input.getAmount())); + compensations.add(Map.entry("refund-payment", + () -> paymentService.refund(payment.getId()))); + + return new OrderResult(true, payment.getOrderId()); + } catch (Exception error) { + ctx.getLogger().error("Order failed, executing compensations", error); + Collections.reverse(compensations); + for (var comp : compensations) { + try { + ctx.step(comp.getKey(), Void.class, stepCtx -> { comp.getValue().run(); return null; }); + } catch (Exception compError) { + ctx.getLogger().error("Compensation {} failed", comp.getKey(), compError); + } + } + throw error; + } + } +} +``` + ## Unrecoverable Errors Mark errors as unrecoverable to stop execution immediately: @@ -285,6 +345,21 @@ The SDK provides these exception types for different failure scenarios: | `CallbackError` | No | Callback handling failures | | `DurableExecutionsError` | — | Base class for all SDK exceptions | +### Java Exception Hierarchy + +``` +DurableExecutionException - General durable exception +├── NonDeterministicExecutionException - Code changed between executions +├── SerDesException - Serialization/deserialization error +└── DurableOperationException - General operation exception + ├── StepFailedException - Step exhausted all retry attempts + ├── StepInterruptedException - AT_MOST_ONCE step interrupted + ├── CallbackFailedException - External system sent error + ├── CallbackTimeoutException - Callback exceeded timeout + ├── WaitForConditionFailedException- Polling exceeded max attempts + └── ChildContextFailedException - Child context failed +``` + ## Error Determinism Ensure errors are deterministic across replays: diff --git a/aws-lambda-durable-functions-power/steering/getting-started.md b/aws-lambda-durable-functions-power/steering/getting-started.md index fa14c90..aa515a8 100644 --- a/aws-lambda-durable-functions-power/steering/getting-started.md +++ b/aws-lambda-durable-functions-power/steering/getting-started.md @@ -5,7 +5,8 @@ Quick start guide for building your first durable function. ## Check user and project preferences Ask which IaC framework to use for new projects. -Ask which programming language to use if unclear, clarify between JavaScript and TypeScript if necessary. +Ask which programming language to use. Supported languages are: **TypeScript**, **JavaScript**, **Python**, and **Java**. If the user says JavaScript or TypeScript, clarify between the two if necessary. +**IMPORTANT**: Only install the SDK, create project files, and show examples for the language the user chose. Do NOT set up other languages. Ask to create a git repo for projects if one doesn't exist already. ## Basic Handler @@ -55,6 +56,29 @@ def handler(event: dict, context: DurableContext) -> dict: return {'success': True, 'data': result} ``` +**Java:** + +```java +import java.time.Duration; +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; + +public class MyHandler extends DurableHandler { + @Override + public MyOutput handleRequest(MyInput input, DurableContext ctx) { + var userData = ctx.step("fetch-user", User.class, + stepCtx -> fetchUserFromDB(input.getUserId())); + + ctx.wait(null, Duration.ofSeconds(5)); + + var result = ctx.step("process", ProcessResult.class, + stepCtx -> processUser(userData)); + + return new MyOutput(true, result); + } +} +``` + ## Common Patterns ### Multi-Step Workflow @@ -124,6 +148,31 @@ def handler(event: dict, context: DurableContext) -> str: messages.append({"role": "assistant", "content": tool_result}) ``` +**Java:** + +```java +public class AIAgentHandler extends DurableHandler { + @Override + public String handleRequest(AgentInput input, DurableContext ctx) { + var messages = new ArrayList>(); + messages.add(Map.of("role", "user", "content", input.getPrompt())); + + while (true) { + var result = ctx.step("invoke-model", AIResponse.class, + stepCtx -> invokeAIModel(messages)); + + if (result.getTool() == null) return result.getResponse(); + + var toolResult = ctx.step("tool-" + result.getTool().getName(), + String.class, + stepCtx -> executeTool(result.getTool(), result.getResponse())); + + messages.add(Map.of("role", "assistant", "content", toolResult)); + } + } +} +``` + ### Human-in-the-Loop Approval **TypeScript:** @@ -176,6 +225,30 @@ def handler(event: dict, context: DurableContext) -> dict: return {'status': 'rejected'} ``` +**Java:** + +```java +public class ApprovalHandler extends DurableHandler { + @Override + public ApprovalResult handleRequest(ApprovalInput input, DurableContext ctx) { + var plan = ctx.step("generate-plan", Plan.class, + stepCtx -> generatePlan(input)); + + var answer = ctx.waitForCallback("wait-for-approval", String.class, + (callbackId, stepCtx) -> sendApprovalEmail(input.getApproverEmail(), plan, callbackId), + WaitForCallbackConfig.builder() + .callbackConfig(CallbackConfig.builder().timeout(Duration.ofHours(24)).build()) + .build()); + + if ("APPROVED".equals(answer)) { + ctx.step("execute", Void.class, stepCtx -> { performAction(plan); return null; }); + return new ApprovalResult("completed"); + } + return new ApprovalResult("rejected"); + } +} +``` + ### Saga Pattern (Compensating Transactions) **TypeScript:** @@ -207,6 +280,36 @@ export const handler = withDurableExecution(async (event, context: DurableContex }); ``` +**Java:** + +```java +public class BookingSaga extends DurableHandler { + @Override + public BookingResult handleRequest(BookingInput input, DurableContext ctx) { + var compensations = new ArrayList>(); + try { + ctx.step("book-flight", Void.class, stepCtx -> { flightClient.book(input); return null; }); + compensations.add(Map.entry("cancel-flight", () -> flightClient.cancel(input))); + + ctx.step("book-hotel", Void.class, stepCtx -> { hotelClient.book(input); return null; }); + compensations.add(Map.entry("cancel-hotel", () -> hotelClient.cancel(input))); + + return new BookingResult(true); + } catch (Exception error) { + Collections.reverse(compensations); + for (var comp : compensations) { + try { + ctx.step(comp.getKey(), Void.class, stepCtx -> { comp.getValue().run(); return null; }); + } catch (Exception compError) { + ctx.getLogger().error("Compensation {} failed", comp.getKey(), compError); + } + } + throw error; + } + } +} +``` + ## Project Structure ### TypeScript @@ -250,7 +353,23 @@ my-durable-function/ └── pyproject.toml # Project configuration ``` -## ESLint Plugin Setup +### Java + +``` +my-durable-function/ +├── src/ +│ ├── main/java/com/example/ +│ │ ├── MyHandler.java # Main handler (extends DurableHandler) +│ │ ├── model/ # Input/output POJOs +│ │ └── service/ # Business logic +│ └── test/java/com/example/ +│ └── MyHandlerTest.java # Tests with LocalDurableTestRunner +├── infrastructure/ +│ └── template.yaml # SAM/CloudFormation +└── pom.xml +``` + +## ESLint Plugin Setup (TypeScript/JavaScript only) Install the ESLint plugin to catch common durable function mistakes at development time: @@ -304,7 +423,7 @@ export default [ - Incorrect usage of durable context outside handler - Common replay model violations -## Jest Configuration +## Jest Configuration (TypeScript only) **jest.config.js:** @@ -333,6 +452,28 @@ module.exports = { ## Python Project Setup Add `aws-durable-execution-sdk-python-testing` to your dev/test dependencies in pyproject.toml. +## Java Project Setup + +Add the SDK and testing dependencies to your `pom.xml`: + +```xml + + + software.amazon.lambda.durable + aws-durable-execution-sdk-java + 1.0.0 + + + software.amazon.lambda.durable + aws-durable-execution-sdk-java-testing + 1.0.0 + test + + +``` + +Java 17+ is required. The handler class extends `DurableHandler` and implements `handleRequest(I input, DurableContext ctx)`. + ## Development Workflow ### TypeScript @@ -351,6 +492,14 @@ Add `aws-durable-execution-sdk-python-testing` to your dev/test dependencies in 4. **Deploy** with qualified ARN (version or alias) 5. **Monitor** execution state and logs +### Java + +1. **Write handler** extending `DurableHandler` +2. **Test locally** with `LocalDurableTestRunner` and JUnit +3. **Validate replay rules** (no non-deterministic code outside steps) +4. **Deploy** with qualified ARN (version or alias) +5. **Monitor** execution state and logs + ## Key Concepts - **Steps**: Atomic operations with automatic retry and checkpointing @@ -386,6 +535,18 @@ When starting a new durable function project: - [ ] Run tests: `pytest` - [ ] Review replay model rules (no non-deterministic code outside steps) +### Java + +- [ ] Add `aws-durable-execution-sdk-java` Maven dependency +- [ ] Add `aws-durable-execution-sdk-java-testing` test dependency +- [ ] Create handler extending `DurableHandler` +- [ ] Implement `handleRequest(I input, DurableContext ctx)` +- [ ] Create SAM template with `DurableConfig` +- [ ] Write tests using `LocalDurableTestRunner.create(InputType.class, handler)` +- [ ] Run tests: `mvn test` +- [ ] Build and deploy: `mvn package && sam build && sam deploy --guided` +- [ ] Review replay model rules (no non-deterministic code outside steps) + ## Error Scenarios ### Unsupported Language diff --git a/aws-lambda-durable-functions-power/steering/replay-model-rules.md b/aws-lambda-durable-functions-power/steering/replay-model-rules.md index dc3a6ee..dbb9a43 100644 --- a/aws-lambda-durable-functions-power/steering/replay-model-rules.md +++ b/aws-lambda-durable-functions-power/steering/replay-model-rules.md @@ -51,6 +51,18 @@ now = datetime.now() # Different datetime each time context.step(lambda _: save_data({"id": id}), name='save') ``` +**Java:** + +```java +// These values change on each replay! +var id = UUID.randomUUID().toString(); +var timestamp = System.currentTimeMillis(); +var random = Math.random(); +var now = Instant.now(); + +ctx.step("save", Void.class, stepCtx -> { saveData(id, timestamp); return null; }); +``` + ### ✅ CORRECT - Non-Deterministic Inside Steps **TypeScript:** @@ -75,11 +87,22 @@ now = context.step(lambda _: datetime.now(), name='get-date') context.step(lambda _: save_data({"id": id}), name='save') ``` +**Java:** + +```java +var id = ctx.step("generate-id", String.class, stepCtx -> UUID.randomUUID().toString()); +var timestamp = ctx.step("get-time", Long.class, stepCtx -> System.currentTimeMillis()); +var random = ctx.step("random", Double.class, stepCtx -> Math.random()); +var now = ctx.step("get-date", String.class, stepCtx -> Instant.now().toString()); + +ctx.step("save", Void.class, stepCtx -> { saveData(id, timestamp); return null; }); +``` + ### Must Be In Steps -- `Date.now()`, `new Date()`, `time.time()`, `datetime.now()` +- `Date.now()`, `new Date()`, `time.time()`, `datetime.now()`, `Instant.now()`, `System.currentTimeMillis()` - `Math.random()`, `random.random()` -- UUID generation (`uuid.v4()`, `uuid.uuid4()`) +- UUID generation (`uuid.v4()`, `uuid.uuid4()`, `UUID.randomUUID()`) - API calls, HTTP requests - Database queries - File system operations @@ -115,6 +138,16 @@ def process(step_ctx: StepContext): context.step(process()) ``` +**Java:** + +```java +ctx.step("process", String.class, stepCtx -> { + ctx.wait(null, Duration.ofSeconds(1)); // ERROR! + ctx.step("nested", String.class, s -> ""); // ERROR! + return "result"; +}); +``` + ### ✅ CORRECT - Use Child Context **TypeScript:** @@ -141,6 +174,17 @@ def process_child(child_ctx: DurableContext): context.run_in_child_context(func=process_child, name='process') ``` +**Java:** + +```java +ctx.runInChildContext("process", String.class, childCtx -> { + childCtx.wait(null, Duration.ofSeconds(1)); + var step1 = childCtx.step("validate", String.class, stepCtx -> validate()); + var step2 = childCtx.step("process", String.class, stepCtx -> process(step1)); + return step2; +}); +``` + ## Rule 3: Closure Mutations Are Lost **Variables mutated inside steps are NOT preserved across replays.** @@ -170,6 +214,18 @@ context.step(increment()) print(counter) # Always 0 on replay! ``` +**Java:** + +```java +// ❌ WRONG +var counter = new AtomicInteger(0); +ctx.step("increment", Void.class, stepCtx -> { counter.incrementAndGet(); return null; }); +// counter is always 0 on replay! + +// ✅ CORRECT +var counter = ctx.step("increment", Integer.class, stepCtx -> 0 + 1); +``` + ### ✅ CORRECT - Return Values **TypeScript:** @@ -235,9 +291,21 @@ context.step(update_database(data)) context.step(process()) ``` +**Java:** + +```java +// ❌ WRONG +System.out.println("Starting process"); // Prints multiple times! +sendEmail(user.getEmail()); // Sends multiple emails! + +// ✅ CORRECT +ctx.getLogger().info("Starting process"); // Deduplicated automatically +ctx.step("send-email", Void.class, stepCtx -> { sendEmail(user.getEmail()); return null; }); +``` + ### Exception: context.logger -`context.logger` is replay-aware and safe to use anywhere. It automatically deduplicates logs across replays. +`context.logger` (TypeScript/Python) and `ctx.getLogger()` (Java) are replay-aware and safe to use anywhere. It automatically deduplicates logs across replays. ## Common Pitfalls diff --git a/aws-lambda-durable-functions-power/steering/step-operations.md b/aws-lambda-durable-functions-power/steering/step-operations.md index 0b3f62d..1c3f4f2 100644 --- a/aws-lambda-durable-functions-power/steering/step-operations.md +++ b/aws-lambda-durable-functions-power/steering/step-operations.md @@ -53,6 +53,26 @@ const result = await context.step('fetch-user', async () => { **Best Practice:** Always name steps for easier debugging and testing. +### Java: Typed Steps + +```java +// Basic step — type class required for deserialization +var result = ctx.step("fetch-user", User.class, + stepCtx -> userService.getUser(userId)); + +// Generic types — use TypeToken for parameterized types +var users = ctx.step("fetch-users", new TypeToken>() {}, + stepCtx -> userService.getAllUsers()); + +// Async steps for concurrency +DurableFuture userFuture = ctx.stepAsync("fetch-user", User.class, + stepCtx -> userService.getUser(userId)); +DurableFuture> ordersFuture = ctx.stepAsync("fetch-orders", + new TypeToken>() {}, stepCtx -> orderService.getOrders(userId)); +User user = userFuture.get(); +List orders = ordersFuture.get(); +``` + ## Retry Configuration ### Exponential Backoff @@ -98,6 +118,17 @@ result = context.step( ) ``` +**Java:** + +```java +var result = ctx.step("api-call", Response.class, + stepCtx -> callExternalAPI(), + StepConfig.builder() + .retryStrategy(RetryStrategies.exponentialBackoff( + 5, Duration.ofSeconds(1), Duration.ofSeconds(60), 2.0, JitterStrategy.FULL)) + .build()); +``` + ### Custom Retry Strategy **TypeScript:** @@ -150,6 +181,20 @@ result = context.step( ) ``` +**Java:** + +```java +var result = ctx.step("custom-retry", Result.class, + stepCtx -> riskyOperation(), + StepConfig.builder() + .retryStrategy((error, attempt) -> { + if (error instanceof ValidationException) return RetryDecision.noRetry(); + if (attempt < 3) return RetryDecision.retryAfter(Duration.ofSeconds((long) Math.pow(2, attempt))); + return RetryDecision.noRetry(); + }) + .build()); +``` + ### Retryable Error Types **TypeScript:** @@ -217,6 +262,25 @@ result = context.step( ) ``` +**Java:** + +```java +// AT_MOST_ONCE_PER_RETRY — non-idempotent operations +var result = ctx.step("charge-payment", Result.class, + stepCtx -> paymentService.charge(amount), + StepConfig.builder() + .semantics(StepSemantics.AT_MOST_ONCE_PER_RETRY) + .build()); + +// True at-most-once: combine with no-retry strategy +var result = ctx.step("charge-payment", Result.class, + stepCtx -> paymentService.charge(amount), + StepConfig.builder() + .semantics(StepSemantics.AT_MOST_ONCE_PER_RETRY) + .retryStrategy(RetryStrategies.Presets.NO_RETRY) + .build()); +``` + ## Custom Serialization For complex types, provide custom serialization: @@ -262,6 +326,22 @@ user = context.step( ) ``` +**Java:** + +```java +// Java SDK uses Jackson by default — POJOs serialize automatically +// For custom serialization, provide a per-step SerDes: +var result = ctx.step("fetch-data", ComplexType.class, + stepCtx -> fetchComplexData(), + StepConfig.builder().serDes(new MyCustomSerDes()).build()); + +// Or configure globally via DurableConfig: +@Override +protected DurableConfig createConfiguration() { + return DurableConfig.builder().withSerDes(new MyCustomSerDes()).build(); +} +``` + ## When to Use Steps vs Child Contexts ### Use Steps For: @@ -336,6 +416,19 @@ except Exception as error: context.logger.error('Application error: %s', str(error)) ``` +**Java:** + +```java +try { + var result = ctx.step("risky", Result.class, stepCtx -> riskyOperation()); +} catch (StepFailedException e) { + ctx.getLogger().error("Step failed after all retries: {}", e.getMessage()); +} catch (StepInterruptedException e) { + // AT_MOST_ONCE step was interrupted — check external state + ctx.getLogger().warn("Step interrupted: {}", e.getOperation().id()); +} +``` + ## Best Practices 1. **Always name steps** for debugging and testing diff --git a/aws-lambda-durable-functions-power/steering/testing-patterns.md b/aws-lambda-durable-functions-power/steering/testing-patterns.md index b132227..1ef4d3c 100644 --- a/aws-lambda-durable-functions-power/steering/testing-patterns.md +++ b/aws-lambda-durable-functions-power/steering/testing-patterns.md @@ -20,6 +20,11 @@ Test durable functions locally and in the cloud with comprehensive test runners. - ✅ Python: Instantiate `DurableFunctionTestRunner(handler=my_handler)` directly - ✅ Python: Use `runner.run(input={...}, timeout=10)` — note `input=` not `payload` - ✅ Python: The value of result.result is serialized. Deserialize using the appropriate SerDes or default json deserializer. +- ✅ Java: Use `result.getOperation("name")` to find operations by name +- ✅ Java: Use `LocalDurableTestRunner.create(InputType.class, handler)` to create runner +- ✅ Java: Use `runner.runUntilComplete(input)` for auto-time-skipping tests +- ✅ Java: Use `result.getResult(OutputType.class)` to get typed results — NOT `getOutput()` +- ✅ Java: Import `ExecutionStatus` from `software.amazon.lambda.durable.model.ExecutionStatus` — NOT from `testing` package ### DON'T: @@ -30,6 +35,8 @@ Test durable functions locally and in the cloud with comprehensive test runners. - ❌ TypeScript: Test callbacks without proper synchronization (leads to race conditions) - ❌ Python: Confuse `DurableFunctionTestRunner` (local) with `DurableFunctionCloudTestRunner` (cloud) - ❌ Python: Forget the `with runner:` context manager — it manages execution lifecycle +- ❌ Java: Use `getOutput()` — the correct method is `getResult(OutputType.class)` +- ❌ Java: Import `ExecutionStatus` from `software.amazon.lambda.durable.testing` — correct package is `software.amazon.lambda.durable.model` ## Local Testing Setup @@ -93,6 +100,24 @@ def test_workflow(): assert result.status is InvocationStatus.SUCCEEDED ``` +**Java:** + +```java +import software.amazon.lambda.durable.model.ExecutionStatus; +import software.amazon.lambda.durable.testing.LocalDurableTestRunner; + +@Test +void testWorkflow() { + var handler = new MyHandler(); + var runner = LocalDurableTestRunner.create(MyInput.class, handler); + + var result = runner.runUntilComplete(new MyInput("123")); + + assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus()); + assertNotNull(result.getResult(MyOutput.class)); +} +``` + ## Getting Operations **CRITICAL: Always get operations by NAME, not by index.** @@ -138,6 +163,29 @@ def test_steps_execute(): assert 'process-data' in step_names ``` +**Java:** + +```java +@Test +void testStepsExecute() { + var runner = LocalDurableTestRunner.create(MyInput.class, handler); + var result = runner.runUntilComplete(input); + + // ✅ CORRECT: Get by name + var fetchOp = result.getOperation("fetch-user"); + assertNotNull(fetchOp); + assertEquals(OperationStatus.SUCCEEDED, fetchOp.getStatus()); + + // Get typed step result + var userData = fetchOp.getStepResult(User.class); + assertNotNull(userData); + + // Inspect all operations + List succeeded = result.getSucceededOperations(); + List failed = result.getFailedOperations(); +} +``` + ## Testing Replay Behavior **TypeScript:** @@ -469,6 +517,16 @@ def test_workflow_cloud(): assert result.status is InvocationStatus.SUCCEEDED ``` +**Java:** + +```java +var runner = CloudDurableTestRunner.create( + "arn:aws:lambda:us-east-1:123456789012:function:order-processor:$LATEST", + Order.class, OrderResult.class); +var result = runner.run(new Order("order-123", items)); +assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus()); +``` + ## Test Assertions **TypeScript:** diff --git a/aws-lambda-durable-functions-power/steering/wait-operations.md b/aws-lambda-durable-functions-power/steering/wait-operations.md index 664c192..55e7cce 100644 --- a/aws-lambda-durable-functions-power/steering/wait-operations.md +++ b/aws-lambda-durable-functions-power/steering/wait-operations.md @@ -32,6 +32,20 @@ context.wait(duration=Duration.from_days(7)) context.wait(duration=Duration.from_seconds(60), name='rate-limit-delay') ``` +**Java:** + +```java +ctx.wait(null, Duration.ofSeconds(30)); +ctx.wait(null, Duration.ofMinutes(5)); +ctx.wait(null, Duration.ofHours(1).plusMinutes(30)); +ctx.wait("rate-limit-delay", Duration.ofSeconds(60)); + +// Async wait — returns DurableFuture, doesn't block +DurableFuture timer = ctx.waitAsync("min-delay", Duration.ofSeconds(5)); +var result = ctx.step("process", String.class, stepCtx -> doWork()); +timer.get(); // block until wait elapses +``` + **Max wait duration:** Up to 1 year ## Wait for Callback @@ -79,6 +93,29 @@ result = context.wait_for_callback( ) ``` +**Java:** + +```java +// waitForCallback (combined creation + submission) +var result = ctx.waitForCallback("wait-for-approval", String.class, + (callbackId, stepCtx) -> sendApprovalEmail(approverEmail, callbackId), + WaitForCallbackConfig.builder() + .callbackConfig(CallbackConfig.builder() + .timeout(Duration.ofHours(24)) + .heartbeatTimeout(Duration.ofMinutes(5)) + .build()) + .build()); + +// Or createCallback (separate creation and waiting) +var callback = ctx.createCallback("approval", String.class, + CallbackConfig.builder().timeout(Duration.ofHours(24)).build()); +ctx.step("send-notification", String.class, stepCtx -> { + notificationService.sendApprovalRequest(callback.callbackId(), requestDetails); + return "sent"; +}); +String approvalResult = callback.get(); // suspends until callback received +``` + ### Callback Success **CLI:** @@ -212,6 +249,24 @@ result = context.wait_for_condition( ) ``` +**Java:** + +```java +var status = ctx.waitForCondition( + "wait-for-shipment", String.class, + (currentStatus, stepCtx) -> { + var latest = orderService.getStatus(orderId); + return "SHIPPED".equals(latest) + ? WaitForConditionResult.stopPolling(latest) + : WaitForConditionResult.continuePolling(latest); + }, + "PENDING", + WaitForConditionConfig.builder() + .waitStrategy(WaitStrategies.exponentialBackoff( + 60, Duration.ofSeconds(5), Duration.ofSeconds(30), 1.5, JitterStrategy.FULL)) + .build()); +``` + ### Custom Wait Strategy **TypeScript:** @@ -394,3 +449,15 @@ except CallbackError as error: else: context.logger.error('Callback failed', error) ``` + +**Java:** + +```java +try { + var result = callback.get(); +} catch (CallbackTimeoutException e) { + ctx.getLogger().warn("Approval timed out"); +} catch (CallbackFailedException e) { + ctx.getLogger().error("Callback failed: {}", e.getMessage()); +} +``` diff --git a/docs/getting-started/development-environment.md b/docs/getting-started/development-environment.md new file mode 100644 index 0000000..d9f16d0 --- /dev/null +++ b/docs/getting-started/development-environment.md @@ -0,0 +1,25 @@ +# Development Environment + +## Development workflow + +```mermaid +flowchart LR + subgraph dev["Development (Local)"] + direction LR + A["1. Write Function"] + B["2. Write Tests"] + C["3. Run Tests"] + end + + subgraph prod["Production (AWS)"] + direction LR + D["4. Deploy"] + E["5. Test in Cloud"] + end + + A --> B --> C --> D --> E + + style dev fill:#e3f2fd + style prod fill:#fff3e0 +``` + diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 65a4147..7a5c824 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -15,5 +15,11 @@ concepts introduced here. - [Key Concepts](key-concepts.md) Understand durable execution, checkpoints, replay, and the DurableContext before writing code -- [Quick Start](quick-start.md) Install the SDK, write your first durable function, and - test it locally +- [Quickstart](quickstart.md) Install the SDK, write your first durable function, and + deploy it with the AWS CLI +- [Quickstart for Container Image](quickstart-container-image.md) Deploy a durable + function in a container image +- [Manage Executions](manage-executions.md) List, inspect, stop, update, and delete + durable functions and executions +- [Development Environment](development-environment.md) Development workflow, SDK + installation, and testing setup diff --git a/docs/getting-started/manage-executions.md b/docs/getting-started/manage-executions.md new file mode 100644 index 0000000..430eb1b --- /dev/null +++ b/docs/getting-started/manage-executions.md @@ -0,0 +1,122 @@ +# Manage Executions + +Use the AWS CLI to inspect, stop, update, and clean up durable functions and their +executions. + +## List executions + +```console +aws lambda list-durable-executions-by-function \ + --function-name my-durable-function +``` + +## Get execution details + +```console +aws lambda get-durable-execution \ + --durable-execution-arn +``` + +## Get execution history + +View the checkpoint history for an execution: + +```console +aws lambda get-durable-execution-history \ + --durable-execution-arn +``` + +## Stop an execution + +```console +aws lambda stop-durable-execution \ + --durable-execution-arn +``` + +## Update function code + +After updating your code, publish a new version and point your alias to it. + +=== "Zip (TypeScript/Python)" + + ```console + aws lambda update-function-code \ + --function-name my-durable-function \ + --zip-file fileb://function.zip + + aws lambda wait function-updated \ + --function-name my-durable-function + + aws lambda publish-version \ + --function-name my-durable-function + + aws lambda update-alias \ + --function-name my-durable-function \ + --name prod \ + --function-version + ``` + +=== "Container image (Java)" + + ```console + aws lambda update-function-code \ + --function-name my-durable-function \ + --image-uri 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-durable-function:latest + + aws lambda wait function-updated \ + --function-name my-durable-function + + aws lambda publish-version \ + --function-name my-durable-function + + aws lambda update-alias \ + --function-name my-durable-function \ + --name prod \ + --function-version + ``` + +Running executions will continue to use the version they started with. New invocations +use the updated alias. + +If you're still actively developing and you don't want to publish a new version each +time you update, you could use `LATEST$` just during development, but please be very +aware that executions might not replay correctly (or even fail) if the underlying code +changes during running executions. Always use numbered versions or aliases in +production. + +## View logs + +```console +aws logs tail /aws/lambda/my-durable-function --follow +``` + +## Delete durable functions + +```console +aws lambda delete-function --function-name my-durable-function + +aws iam detach-role-policy \ + --role-name durable-function-role \ + --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicDurableExecutionRolePolicy + +aws iam delete-role --role-name durable-function-role +``` + +If you deployed as a container image, also +[delete the image](https://docs.aws.amazon.com/AmazonECR/latest/userguide/delete_image.html) +from ECR: + +```console +aws ecr batch-delete-image \ + --repository-name my-durable-function \ + --image-ids imageTag=latest +``` + +Replace `latest` with the tag you pushed if you used a different tag. To delete multiple +tags at once, specify each with a separate `imageTag=` argument: + +```console +aws ecr batch-delete-image \ + --repository-name my-durable-function \ + --image-ids imageTag=latest imageTag=v1.0.0 +``` diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md deleted file mode 100644 index 9260011..0000000 --- a/docs/getting-started/quick-start.md +++ /dev/null @@ -1,288 +0,0 @@ -# Getting started - -## Table of Contents - -- [Overview](#overview) -- [The two SDKs](#the-two-sdks) -- [How durable execution works](#how-durable-execution-works) -- [Your development workflow](#your-development-workflow) -- [Quick start](#quick-start) -- [Next steps](#next-steps) - -[← Back to Get Started](index.md) - -## Overview - -This guide explains the fundamental concepts behind durable execution and how the SDK works. You'll understand: - -- The difference between `aws-durable-execution-sdk-python` and `aws-durable-execution-sdk-python-testing` -- How checkpoints and replay enable reliable workflows -- Why your function code runs multiple times but side effects happen once -- The development workflow from writing to testing to deployment - -[↑ Back to top](#table-of-contents) - -## The two SDKs - -The durable execution ecosystem has two separate packages: - -### Execution SDK (aws-durable-execution-sdk-python) - -This is the **core SDK** that runs in your Lambda functions. It provides: - -- `DurableContext` - The main interface for durable operations -- Operations - Steps, waits, callbacks, parallel, map, child contexts -- Decorators - `@durable_execution`, `@durable_step`, etc. -- Configuration - StepConfig, CallbackConfig, retry strategies -- Serialization - How data is saved in checkpoints - -Install it in your Lambda deployment package: - -```console -pip install aws-durable-execution-sdk-python -``` - -### Testing SDK (aws-durable-execution-sdk-python-testing) - -This is a **separate SDK** for testing your durable functions. It provides: - -- `DurableFunctionTestRunner` - Run functions locally without AWS -- `DurableFunctionCloudTestRunner` - Test deployed Lambda functions -- Pytest integration - Fixtures and markers for writing tests -- Result inspection - Examine execution state and operation results - -Install it in your development environment only: - -```console -pip install aws-durable-execution-sdk-python-testing -``` - -**Key distinction:** The execution SDK runs in production Lambda. The testing SDK runs on your laptop or CI/CD. They're separate concerns. - -[↑ Back to top](#table-of-contents) - -## How durable execution works - -Let's trace through a simple workflow to understand the execution model: - -=== "TypeScript" - - ``` typescript - --8<-- "examples/typescript/getting-started/execution-model.ts" - ``` - -=== "Python" - - ``` python - --8<-- "examples/python/getting-started/execution-model.py" - ``` - -=== "Java" - - ``` java - --8<-- "examples/java/getting-started/execution-model.java" - ``` - -**First invocation (t=0s):** - -1. Lambda invokes your function -2. `fetch_data` executes and calls an external API -3. Result is checkpointed to AWS -4. `context.wait(Duration.from_seconds(30))` is reached -5. Function returns, Lambda can recycle the environment - -**Second invocation (t=30s):** - -1. Lambda invokes your function again -2. Function code runs from the beginning -3. `fetch_data` returns the checkpointed result instantly (no API call) -4. `context.wait(Duration.from_seconds(30))` is already complete, execution continues -5. `process_data` executes for the first time -6. Result is checkpointed -7. Function returns the final result - -**Key insights:** - -- Your function code runs twice, but `fetch_data` only calls the API once -- The wait doesn't block Lambda - your environment can be recycled -- You write linear code that looks synchronous -- The SDK handles all the complexity of state management - -[↑ Back to top](#table-of-contents) - -## Your development workflow - -```mermaid -flowchart LR - subgraph dev["Development (Local)"] - direction LR - A["1. Write Function
aws-durable-execution-sdk-python"] - B["2. Write Tests
aws-durable-execution-sdk-python-testing"] - C["3. Run Tests
pytest"] - end - - subgraph prod["Production (AWS)"] - direction LR - D["4. Deploy
SAM/CDK/Terraform"] - E["5. Test in Cloud
pytest --runner-mode=cloud"] - end - - A --> B --> C --> D --> E - - style dev fill:#e3f2fd - style prod fill:#fff3e0 -``` - -Here's how you build and test durable functions: - -### 1. Write your function (execution SDK) - -Install the execution SDK and write your Lambda handler: - -```console -pip install aws-durable-execution-sdk-python -``` - -=== "TypeScript" - - ``` typescript - --8<-- "examples/typescript/getting-started/write-function.ts" - ``` - -=== "Python" - - ``` python - --8<-- "examples/python/getting-started/write-function.py" - ``` - -=== "Java" - - ``` java - --8<-- "examples/java/getting-started/write-function.java" - ``` - -### 2. Test locally (testing SDK) - -Install the testing SDK and write tests: - -```console -pip install aws-durable-execution-sdk-python-testing -``` - -=== "TypeScript" - - ``` typescript - --8<-- "examples/typescript/getting-started/test-locally.ts" - ``` - -=== "Python" - - ``` python - --8<-- "examples/python/getting-started/test-locally.py" - ``` - -=== "Java" - - ``` java - --8<-- "examples/java/getting-started/test-locally.java" - ``` - -Run tests without AWS credentials: - -```console -pytest test_my_function.py -``` - -### 3. Deploy to Lambda - -Package your function with the execution SDK (not the testing SDK) and deploy using your preferred tool (SAM, CDK, Terraform, etc.). - -### 4. Test in the cloud (optional) - -Run the same tests against your deployed function: - -```console -export AWS_REGION=us-west-2 -export QUALIFIED_FUNCTION_NAME="MyFunction:$LATEST" -export LAMBDA_FUNCTION_TEST_NAME="my_function" - -pytest --runner-mode=cloud test_my_function.py -``` - -[↑ Back to top](#table-of-contents) - -## Quick start - -Ready to build your first durable function? Here's a minimal example: - -=== "TypeScript" - - ``` typescript - --8<-- "examples/typescript/getting-started/minimal-example.ts" - ``` - -=== "Python" - - ``` python - --8<-- "examples/python/getting-started/minimal-example.py" - ``` - -=== "Java" - - ``` java - --8<-- "examples/java/getting-started/minimal-example.java" - ``` - -Deploy this to Lambda and you have a durable function. The `greet_user` step is checkpointed automatically. - -### Using a custom boto3 Lambda client - -If you need to customize the boto3 Lambda client used for durable execution operations (for example, to configure custom endpoints, retry settings, or credentials), you can pass a `boto3_client` parameter to the decorator. The client must be a boto3 Lambda client: - -=== "TypeScript" - - ``` typescript - --8<-- "examples/typescript/getting-started/minimal-example.ts" - ``` - -=== "Python" - - ``` python - --8<-- "examples/python/getting-started/minimal-example.py" - ``` - -=== "Java" - - ``` java - --8<-- "examples/java/getting-started/minimal-example.java" - ``` - -The custom Lambda client is used for all checkpoint and state management operations. If you don't provide a `boto3_client`, the SDK initializes a default Lambda client from your environment. - -[↑ Back to top](#table-of-contents) - -## Next steps - -Now that you've built your first durable function, explore the core features: - -**Learn the operations:** -- [Steps](../sdk-reference/operations/step.md) - Execute code with retry strategies and checkpointing -- [Wait operations](../sdk-reference/operations/wait.md) - Pause execution for seconds, minutes, or hours -- [Callbacks](../sdk-reference/operations/callback.md) - Wait for external systems to respond -- [Child contexts](../sdk-reference/operations/child-context.md) - Organize complex workflows -- [Parallel operations](../sdk-reference/operations/parallel.md) - Run multiple operations concurrently -- [Map operations](../sdk-reference/operations/map.md) - Process collections in parallel - -**Dive deeper:** -- [Error handling](../sdk-reference/error-handling/errors.md) - Handle failures and implement retry strategies -- [Testing patterns](../testing/basic-tests.md) - Write effective tests for your workflows -- [Best practices](../patterns/best-practices.md) - Avoid common pitfalls - -[↑ Back to top](#table-of-contents) - -## See also - -- [Documentation index](../index.md) - Browse all guides and examples -- [Logger integration](../sdk-reference/observability/logging.md) - Replay-safe structured logging - -[↑ Back to top](#table-of-contents) diff --git a/docs/getting-started/quickstart-container-image.md b/docs/getting-started/quickstart-container-image.md new file mode 100644 index 0000000..2d7c2d4 --- /dev/null +++ b/docs/getting-started/quickstart-container-image.md @@ -0,0 +1,142 @@ +# Quickstart for Container Image with Java + +Deploy your first durable function as a container image using the AWS CLI. + +You can deploy any of the supported SDK languages in a container image. + +This quickstart shows how to do so with Java. + +## Prerequisites + +- AWS CLI installed and configured with credentials +- Docker installed and running +- An Amazon ECR repository to push your image to +- Java 17+ and Maven 3.8+ + +## Create the execution role + +Follow the [execution role setup](quickstart.md#create-the-execution-role) in the main +quickstart, then return here. + +## Write the function + +=== "Java" + + ```java + --8<-- "examples/java/getting-started/quickstart.java" + ``` + +## Set up your Maven project + +Add to your `pom.xml` dependencies: + +```xml + + software.amazon.lambda.durable + aws-durable-execution-sdk-java + + + com.amazonaws + aws-lambda-java-core + +``` + +Add the `maven-shade-plugin` to produce a fat jar with all dependencies bundled: + +```xml + + org.apache.maven.plugins + maven-shade-plugin + 3.6.2 + + false + + + + package + shade + + + +``` + +## Build and push the container image + +Create a `Dockerfile` using a multi-stage build: + +```dockerfile +FROM --platform=linux/amd64 amazoncorretto:21-alpine AS builder +WORKDIR /build +COPY pom.xml . +COPY src ./src +RUN apk add --no-cache maven && mvn clean package -DskipTests + +FROM public.ecr.aws/lambda/java:21 +COPY --from=builder /build/target/*.jar ${LAMBDA_TASK_ROOT}/lib/ +CMD ["QuickstartFunction::handleRequest"] +``` + +Authenticate, build, and push to ECR: + +```console +aws ecr get-login-password --region us-east-1 \ + | docker login --username AWS --password-stdin \ + 123456789012.dkr.ecr.us-east-1.amazonaws.com + +docker build -t my-durable-function . + +docker tag my-durable-function:latest \ + 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-durable-function:latest + +docker push \ + 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-durable-function:latest +``` + +Replace `123456789012` and `us-east-1` with your account ID and region. + +## Deploy + +Replace `123456789012` with your AWS account ID and the role arn with that of the +execution role you just created. + +```console +aws lambda create-function \ + --function-name my-durable-function \ + --package-type Image \ + --code ImageUri=123456789012.dkr.ecr.us-east-1.amazonaws.com/my-durable-function:latest \ + --role arn:aws:iam::123456789012:role/durable-function-role \ + --architectures x86_64 \ + --durable-config '{"ExecutionTimeout": 900, "RetentionPeriodInDays": 1}' +``` + +## Publish a version + +```console +aws lambda publish-version --function-name my-durable-function +``` + +## Invoke + +```console +aws lambda invoke \ + --function-name my-durable-function:1 \ + --cli-binary-format raw-in-base64-out \ + --payload '{}' \ + response.json + +cat response.json +``` + +## Clean up + +See [delete durable functions](manage-executions.md#delete-durable-functions) to clean +up your function and IAM role. + +## Next steps + +- [Manage Executions](manage-executions.md) list, inspect, stop, update, and clean up +- [Development Environment](development-environment.md) write and run tests locally + before deploying +- [Key Concepts](key-concepts.md) understand replay, checkpoints, and determinism +- [Steps](../sdk-reference/operations/step.md) retry strategies and checkpointing +- [Wait](../sdk-reference/operations/wait.md) pause execution up to a year diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md new file mode 100644 index 0000000..42111e0 --- /dev/null +++ b/docs/getting-started/quickstart.md @@ -0,0 +1,182 @@ +# Quickstart + +Create and deploy your first durable function using the AWS CLI. This guide covers +TypeScript and Python. + +!!! note "Adding all your dependencies to the deployment package" + + This guide shows you how to package all your dependencies, including the Durable + Execution SDK, and deploy together with your custom code as a zip archive. This ensures + that you control the exact version of the Durable Execution SDK that your code uses. You + can create a durable function for quick testing purposes in the AWS Console, but then + the version of the SDK might be older and it might not contain the latest features and + optimizations. + +## Prerequisites + +- AWS CLI installed and configured with credentials + +=== "TypeScript" + + - Node.js 22+ + +=== "Python" + + - Python 3.13+ + +## Create the execution role + +Create an IAM role that grants your function permission to perform checkpoint +operations. + +Save the following as `trust-policy.json`: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +} +``` + +Create the role and attach the +[AWSLambdaBasicDurableExecutionRolePolicy](https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AWSLambdaBasicDurableExecutionRolePolicy.html) +managed policy: + +```console +# Replace durable-function-role with your preferred role name +aws iam create-role \ + --role-name durable-function-role \ + --assume-role-policy-document file://trust-policy.json + +aws iam attach-role-policy \ + --role-name durable-function-role \ + --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicDurableExecutionRolePolicy +``` + +Note the role ARN returned. You'll need it in the next step. + +## Write the durable function + +!!! note "Using Java?" + + Java durable functions currently only deploy in container images. See + [Quickstart for Container Image](quickstart-container-image.md). + +=== "TypeScript" + + Save as `index.mjs` + + ```typescript + --8<-- "examples/typescript/getting-started/quickstart.ts" + ``` + +=== "Python" + + Save as `lambda_function.py` + + ```python + --8<-- "examples/python/getting-started/quickstart.py" + ``` + +The wait here is for 10 seconds just for an easy quick example, but it could as easily +be 10 days without incurring extra compute. + +## Package and deploy + +Replace `123456789012` with your AWS account ID and the role arn with that of the +execution role you just created. + +=== "TypeScript" + + ```console + mkdir my-function && cd my-function + npm init -y + npm install @aws/durable-execution-sdk-js + ``` + + Save the function code above as `index.mjs`, then package and deploy: + + ```console + zip -r function.zip index.mjs node_modules/ + + aws lambda create-function \ + --function-name my-durable-function \ + --runtime nodejs22.x \ + --role arn:aws:iam::123456789012:role/durable-function-role \ + --handler index.handler \ + --zip-file fileb://function.zip \ + --durable-config '{"ExecutionTimeout": 900, "RetentionPeriodInDays": 1}' + ``` + +=== "Python" + + ```console + mkdir -p package + pip install aws-durable-execution-sdk-python --target package/ + cp lambda_function.py package/ + cd package && zip -r ../function.zip . && cd .. + + aws lambda create-function \ + --function-name my-durable-function \ + --runtime python3.14 \ + --role arn:aws:iam::123456789012:role/durable-function-role \ + --handler lambda_function.lambda_handler \ + --zip-file fileb://function.zip \ + --durable-config '{"ExecutionTimeout": 900, "RetentionPeriodInDays": 1}' + ``` + +### Publish a version + +You must invoke a durable functions with a published version or alias to ensure +deterministic replay. + +For quick testing here in the Quickstart we can just invoke the durable function with +`$LATEST`. Note that you should NOT do this for production workloads. + +Be sure to publish a version if this is for production. Invoking `$LATEST` directly is +not supported for production workloads. + +```console +aws lambda publish-version --function-name my-durable-function +``` + +Note the version number in the returned ARN (for example, `:1`). + +## Invoke + +For synchronous invocation: + +```console +aws lambda invoke \ + --function-name 'my-durable-function:$LATEST' \ + --cli-binary-format raw-in-base64-out \ + --payload '{}' \ + response.json + +cat response.json +``` + +The function runs `step-1`, then pauses for 10 seconds without consuming compute. After +the wait, it resumes and returns the result. + +## Clean up + +See [delete durable functions](manage-executions.md#delete-durable-functions) to clean +up your function and IAM role. + +## Next steps + +- [Manage Executions](manage-executions.md) list, inspect, stop, update, and clean up +- [Development Environment](development-environment.md) write and run tests locally + before deploying +- [Key Concepts](key-concepts.md) understand replay, checkpoints, and determinism +- [Steps](../sdk-reference/operations/step.md) retry strategies and checkpointing +- [Wait](../sdk-reference/operations/wait.md) pause execution up to a year diff --git a/docs/sdk-reference/configuration/custom-lambda-client.md b/docs/sdk-reference/configuration/custom-lambda-client.md new file mode 100644 index 0000000..7d1e962 --- /dev/null +++ b/docs/sdk-reference/configuration/custom-lambda-client.md @@ -0,0 +1,32 @@ +# Configuration + +## Custom Lambda client + +By default, the SDK initializes a Lambda client from your environment. You can provide a +custom client to control the region, retry settings, credentials, or other options. + +=== "TypeScript" + + Pass a `LambdaClient` instance via the `config` parameter of `withDurableExecution`. + + ```typescript + --8<-- "examples/typescript/configuration/custom-client.ts" + ``` + +=== "Python" + + Pass a boto3 Lambda client via the `boto3_client` parameter of `@durable_execution`. The + client must be a boto3 Lambda client. + + ```python + --8<-- "examples/python/configuration/custom-client.py" + ``` + +=== "Java" + + Override `createConfiguration()` in your `DurableHandler` subclass and use + `DurableConfig.builder().withLambdaClientBuilder(...)`. + + ```java + --8<-- "examples/java/configuration/custom-client.java" + ``` diff --git a/docs/sdk-reference/configuration/index.md b/docs/sdk-reference/configuration/index.md new file mode 100644 index 0000000..fe92b62 --- /dev/null +++ b/docs/sdk-reference/configuration/index.md @@ -0,0 +1,4 @@ +# Configuration + +- [Custom Lambda Client](custom-lambda-client.md) Configure the Lambda client used by + the SDK diff --git a/docs/sdk-reference/index.md b/docs/sdk-reference/index.md index aad618e..0227ee5 100644 --- a/docs/sdk-reference/index.md +++ b/docs/sdk-reference/index.md @@ -30,6 +30,11 @@ The core building blocks for constructing durable workflows: - [Logging](observability/logging.md) Structured logging within durable functions +## Configuration + +- [Custom Lambda Client](configuration/custom-lambda-client.md) Configure the Lambda + client used by the SDK + ## Language Guides Language-specific installation and configuration: diff --git a/examples/java/configuration/custom-client.java b/examples/java/configuration/custom-client.java new file mode 100644 index 0000000..85e9067 --- /dev/null +++ b/examples/java/configuration/custom-client.java @@ -0,0 +1,32 @@ +import java.util.Map; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.retries.AdaptiveRetryStrategy; +import software.amazon.awssdk.retries.api.RetryStrategy; +import software.amazon.awssdk.services.lambda.LambdaClient; +import software.amazon.lambda.durable.DurableConfig; +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; + +public class CustomLambdaClient extends DurableHandler, Map> { + + @Override + protected DurableConfig createConfiguration() { + RetryStrategy retryStrategy = AdaptiveRetryStrategy.builder() + .maxAttempts(5) + .build(); + + LambdaClient.Builder lambdaClientBuilder = LambdaClient.builder() + .region(Region.US_WEST_2) + .overrideConfiguration(o -> o.retryStrategy(retryStrategy)); + + return DurableConfig.builder() + .withLambdaClientBuilder(lambdaClientBuilder) + .build(); + } + + @Override + public Map handleRequest(Map event, DurableContext context) { + // Your durable function logic + return Map.of("status", "success"); + } +} diff --git a/examples/java/getting-started/minimal-example.java b/examples/java/getting-started/minimal-example.java deleted file mode 100644 index 48cdd22..0000000 --- a/examples/java/getting-started/minimal-example.java +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/java/getting-started/quickstart.java b/examples/java/getting-started/quickstart.java new file mode 100644 index 0000000..dfc5dc4 --- /dev/null +++ b/examples/java/getting-started/quickstart.java @@ -0,0 +1,23 @@ +import java.time.Duration; +import java.util.Map; +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.DurableHandler; + +public class QuickstartFunction extends DurableHandler, Map> { + + @Override + public Map handleRequest(Map event, DurableContext context) { + String message = context.step("step-1", String.class, stepCtx -> { + stepCtx.getLogger().info("Hello from step-1"); + return "Hello from Durable Lambda!"; + }); + + // Pause for 10 seconds without consuming CPU or incurring usage charges + context.wait("wait-10s", Duration.ofSeconds(10)); + + // Replay-aware: logs once even though the function replays after the wait + context.getLogger().info("Resumed after wait"); + + return Map.of("statusCode", 200, "body", message); + } +} diff --git a/examples/java/getting-started/test-locally.java b/examples/java/getting-started/test-locally.java index 48cdd22..740a436 100644 --- a/examples/java/getting-started/test-locally.java +++ b/examples/java/getting-started/test-locally.java @@ -1 +1,24 @@ -// Coming soon... +import java.util.Map; +import org.junit.jupiter.api.Test; +import software.amazon.lambda.durable.DurableContext; +import software.amazon.lambda.durable.model.ExecutionStatus; +import software.amazon.lambda.durable.testing.LocalDurableTestRunner; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TestLocally { + + @Test + void testMyFunction() { + var runner = LocalDurableTestRunner.create( + Map.class, + (Map input, DurableContext context) -> + context.step("my-step", String.class, + stepCtx -> "processed-" + input.get("data"))); + + var result = runner.runUntilComplete(Map.of("data", "test")); + + assertEquals(ExecutionStatus.SUCCEEDED, result.getStatus()); + assertEquals("processed-test", result.getResult()); + } +} diff --git a/examples/java/getting-started/write-function.java b/examples/java/getting-started/write-function.java deleted file mode 100644 index 48cdd22..0000000 --- a/examples/java/getting-started/write-function.java +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/python/configuration/custom-client.py b/examples/python/configuration/custom-client.py new file mode 100644 index 0000000..d1ce2b9 --- /dev/null +++ b/examples/python/configuration/custom-client.py @@ -0,0 +1,18 @@ +import boto3 +from botocore.config import Config +from aws_durable_execution_sdk_python import durable_execution, DurableContext + +# Create a custom boto3 Lambda client with specific configuration +custom_lambda_client = boto3.client( + 'lambda', + config=Config( + retries={'max_attempts': 5, 'mode': 'adaptive'}, + connect_timeout=10, + read_timeout=60, + ) +) + +@durable_execution(boto3_client=custom_lambda_client) +def handler(event: dict, context: DurableContext) -> dict: + # Your durable function logic + return {"status": "success"} diff --git a/examples/python/getting-started/minimal-example.py b/examples/python/getting-started/minimal-example.py deleted file mode 100644 index a727f42..0000000 --- a/examples/python/getting-started/minimal-example.py +++ /dev/null @@ -1,18 +0,0 @@ -from aws_durable_execution_sdk_python import ( - DurableContext, - durable_execution, - durable_step, - StepContext, -) - -@durable_step -def greet_user(step_context: StepContext, name: str) -> str: - """Generate a greeting.""" - return f"Hello {name}!" - -@durable_execution -def handler(event: dict, context: DurableContext) -> str: - """Simple durable function.""" - name = event.get("name", "World") - greeting = context.step(greet_user(name)) - return greeting diff --git a/examples/python/getting-started/quickstart.py b/examples/python/getting-started/quickstart.py new file mode 100644 index 0000000..5a9d6a4 --- /dev/null +++ b/examples/python/getting-started/quickstart.py @@ -0,0 +1,22 @@ +from aws_durable_execution_sdk_python.config import Duration +from aws_durable_execution_sdk_python.context import DurableContext, StepContext, durable_step +from aws_durable_execution_sdk_python.execution import durable_execution + + +@durable_step +def my_step(step_context: StepContext) -> str: + step_context.logger.info("Hello from my_step") + return "Hello from Durable Lambda!" + + +@durable_execution +def lambda_handler(event, context: DurableContext) -> dict: + message: str = context.step(my_step()) + + # Pause for 10 seconds without consuming CPU or incurring usage charges + context.wait(Duration.from_seconds(10)) + + # Replay-aware: logs once even though the function replays after the wait + context.logger.info("Resumed after wait") + + return {"statusCode": 200, "body": message} diff --git a/examples/python/getting-started/write-function.py b/examples/python/getting-started/write-function.py index bef7571..1ded10f 100644 --- a/examples/python/getting-started/write-function.py +++ b/examples/python/getting-started/write-function.py @@ -2,14 +2,17 @@ DurableContext, durable_execution, durable_step, + StepContext, ) + @durable_step -def my_step(step_context, data): - # Your business logic - return result +def my_step(step_context: StepContext, data: str) -> str: + # Your business logic here + return f"processed-{data}" + @durable_execution -def handler(event, context: DurableContext): +def handler(event: dict, context: DurableContext) -> str: result = context.step(my_step(event["data"])) return result diff --git a/examples/typescript/configuration/custom-client.ts b/examples/typescript/configuration/custom-client.ts new file mode 100644 index 0000000..61711f0 --- /dev/null +++ b/examples/typescript/configuration/custom-client.ts @@ -0,0 +1,22 @@ +import { LambdaClient } from "@aws-sdk/client-lambda"; +import { + DurableContext, + withDurableExecution, + DurableExecutionConfig, +} from "@aws/durable-execution-sdk-js"; + +const customClient = new LambdaClient({ + region: "us-west-2", + maxAttempts: 5, + retryMode: "adaptive", +}); + +const config: DurableExecutionConfig = { client: customClient }; + +export const handler = withDurableExecution( + async (event: Record, context: DurableContext) => { + // Your durable function logic + return { status: "success" }; + }, + config, +); diff --git a/examples/typescript/getting-started/custom-client.ts b/examples/typescript/getting-started/custom-client.ts new file mode 100644 index 0000000..61711f0 --- /dev/null +++ b/examples/typescript/getting-started/custom-client.ts @@ -0,0 +1,22 @@ +import { LambdaClient } from "@aws-sdk/client-lambda"; +import { + DurableContext, + withDurableExecution, + DurableExecutionConfig, +} from "@aws/durable-execution-sdk-js"; + +const customClient = new LambdaClient({ + region: "us-west-2", + maxAttempts: 5, + retryMode: "adaptive", +}); + +const config: DurableExecutionConfig = { client: customClient }; + +export const handler = withDurableExecution( + async (event: Record, context: DurableContext) => { + // Your durable function logic + return { status: "success" }; + }, + config, +); diff --git a/examples/typescript/getting-started/minimal-example.ts b/examples/typescript/getting-started/minimal-example.ts deleted file mode 100644 index 48cdd22..0000000 --- a/examples/typescript/getting-started/minimal-example.ts +++ /dev/null @@ -1 +0,0 @@ -// Coming soon... diff --git a/examples/typescript/getting-started/quickstart.ts b/examples/typescript/getting-started/quickstart.ts new file mode 100644 index 0000000..8233418 --- /dev/null +++ b/examples/typescript/getting-started/quickstart.ts @@ -0,0 +1,16 @@ +import { withDurableExecution } from "@aws/durable-execution-sdk-js"; + +export const handler = withDurableExecution(async (event, context) => { + const message = await context.step("step-1", (stepCtx) => { + stepCtx.logger.info("Hello from step-1"); + return "Hello from Durable Lambda!"; + }); + + // Pause for 10 seconds without consuming CPU or incurring usage charges + await context.wait({ seconds: 10 }); + + // Replay-aware: logs once even though the function replays after the wait + context.logger.info("Resumed after wait"); + + return { statusCode: 200, body: message }; +}); diff --git a/examples/typescript/getting-started/test-locally.ts b/examples/typescript/getting-started/test-locally.ts index 48cdd22..6f5c811 100644 --- a/examples/typescript/getting-started/test-locally.ts +++ b/examples/typescript/getting-started/test-locally.ts @@ -1 +1,20 @@ -// Coming soon... +import { LocalDurableTestRunner } from "@aws/durable-execution-sdk-js-testing"; +import { ExecutionStatus } from "@aws-sdk/client-lambda"; +import { handler } from "./write-function"; + +beforeAll(async () => { + await LocalDurableTestRunner.setupTestEnvironment(); +}); + +afterAll(async () => { + await LocalDurableTestRunner.teardownTestEnvironment(); +}); + +test("my function", async () => { + const runner = new LocalDurableTestRunner({ handlerFunction: handler }); + + const result = await runner.run({ payload: { data: "test" } }); + + expect(result.getStatus()).toBe(ExecutionStatus.SUCCEEDED); + expect(result.getResult()).toBe("processed-test"); +}); diff --git a/examples/typescript/getting-started/write-function.ts b/examples/typescript/getting-started/write-function.ts index 48cdd22..98d9f92 100644 --- a/examples/typescript/getting-started/write-function.ts +++ b/examples/typescript/getting-started/write-function.ts @@ -1 +1,14 @@ -// Coming soon... +import { + DurableContext, + withDurableExecution, +} from "@aws/durable-execution-sdk-js"; + +export const handler = withDurableExecution( + async (event: { data: string }, context: DurableContext) => { + const result = await context.step("my-step", async () => { + // Your business logic here + return `processed-${event.data}`; + }); + return result; + }, +); diff --git a/zensical.toml b/zensical.toml index 9ebfce3..876def3 100644 --- a/zensical.toml +++ b/zensical.toml @@ -52,7 +52,10 @@ nav = [ { "Getting Started" = [ { "Getting Started" = "getting-started/index.md" }, { "Key Concepts" = "getting-started/key-concepts.md" }, - { "Quick Start" = "getting-started/quick-start.md" }, + { "Quickstart" = "getting-started/quickstart.md" }, + { "Quickstart for Container Image" = "getting-started/quickstart-container-image.md" }, + { "Manage Executions" = "getting-started/manage-executions.md" }, + { "Development Environment" = "getting-started/development-environment.md" }, ]}, { "SDK Reference" = [ { "SDK Reference" = "sdk-reference/index.md" }, @@ -76,6 +79,10 @@ nav = [ { "Observability" = [ { "Logging" = "sdk-reference/observability/logging.md" }, ]}, + { "Configuration" = [ + { "Configuration" = "sdk-reference/configuration/index.md" }, + { "Custom Lambda Client" = "sdk-reference/configuration/custom-lambda-client.md" }, + ]}, { "Language Guides" = [ { "Language Guides" = "sdk-reference/languages/index.md" }, { "TypeScript" = "sdk-reference/languages/typescript/index.md" },