From eea33e33d01cfa5c1cc0229849586138756d54ba Mon Sep 17 00:00:00 2001 From: Frank Chen Date: Fri, 10 Jul 2026 13:21:26 -0700 Subject: [PATCH 1/2] chore: generate examples SAM template --- .../cleanup_e2e_unmanaged_log_groups.sh | 133 ++++ .github/workflows/e2e-tests.yml | 8 + .gitignore | 4 +- examples/README.md | 11 + examples/build-and-run.sh | 1 + examples/generate-template.py | 233 +++++++ .../durable/examples/ExampleTemplate.java | 17 + .../examples/otel/OtelXRayStepExample.java | 2 + .../examples/otel/OtelXRayWaitExample.java | 2 + ...anyAsyncStepsVirtualThreadPoolExample.java | 2 + examples/template.yaml | 617 ------------------ 11 files changed, 412 insertions(+), 618 deletions(-) create mode 100755 .github/scripts/cleanup_e2e_unmanaged_log_groups.sh create mode 100755 examples/generate-template.py create mode 100644 examples/src/main/java/software/amazon/lambda/durable/examples/ExampleTemplate.java delete mode 100644 examples/template.yaml diff --git a/.github/scripts/cleanup_e2e_unmanaged_log_groups.sh b/.github/scripts/cleanup_e2e_unmanaged_log_groups.sh new file mode 100755 index 000000000..9a62265a8 --- /dev/null +++ b/.github/scripts/cleanup_e2e_unmanaged_log_groups.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: cleanup_e2e_unmanaged_log_groups.sh [--execute] [--region REGION] [--stack-name NAME ...] + +Deletes Lambda CloudWatch log groups for e2e test stacks only when those log +groups are not already managed as AWS::Logs::LogGroup resources by the stack. + +This is intended for the one-time migration from Lambda auto-created log groups +to CloudFormation-managed log groups with explicit retention. The default mode is +a dry run. Pass --execute to delete matching unmanaged log groups. +USAGE +} + +execute=false +region="${AWS_REGION:-}" +stacks=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --execute) + execute=true + shift + ;; + --region) + region="${2:?--region requires a value}" + shift 2 + ;; + --stack-name) + stacks+=("${2:?--stack-name requires a value}") + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if [[ ${#stacks[@]} -eq 0 ]]; then + stacks=( + Java17-JavaSDKCloudBasedIntegrationTestStack + Java21-JavaSDKCloudBasedIntegrationTestStack + Java25-JavaSDKCloudBasedIntegrationTestStack + ) +fi + +aws_args=() +if [[ -n "$region" ]]; then + aws_args+=(--region "$region") +fi + +log_group_exists() { + local target="$1" + local found + + found=$(aws "${aws_args[@]}" logs describe-log-groups \ + --log-group-name-prefix "$target" \ + --query 'logGroups[].logGroupName' \ + --output text) + + for log_group_name in $found; do + if [[ "$log_group_name" == "$target" ]]; then + return 0 + fi + done + + return 1 +} + +is_managed_log_group() { + local target="$1" + shift + + for managed_log_group in "$@"; do + if [[ "$managed_log_group" == "$target" ]]; then + return 0 + fi + done + + return 1 +} + +for stack in "${stacks[@]}"; do + if ! aws "${aws_args[@]}" cloudformation describe-stacks --stack-name "$stack" >/dev/null 2>&1; then + echo "Skipping missing stack: $stack" + continue + fi + + function_names_text=$(aws "${aws_args[@]}" cloudformation list-stack-resources \ + --stack-name "$stack" \ + --query "StackResourceSummaries[?ResourceType=='AWS::Lambda::Function'].PhysicalResourceId" \ + --output text) + managed_log_groups_text=$(aws "${aws_args[@]}" cloudformation list-stack-resources \ + --stack-name "$stack" \ + --query "StackResourceSummaries[?ResourceType=='AWS::Logs::LogGroup'].PhysicalResourceId" \ + --output text) + + function_names=($function_names_text) + managed_log_groups=($managed_log_groups_text) + + for function_name in "${function_names[@]}"; do + log_group="/aws/lambda/${function_name}" + + if is_managed_log_group "$log_group" "${managed_log_groups[@]}"; then + echo "Keeping managed log group: $log_group" + continue + fi + + if ! log_group_exists "$log_group"; then + echo "No unmanaged log group found: $log_group" + continue + fi + + if [[ "$execute" == true ]]; then + echo "Deleting unmanaged log group: $log_group" + aws "${aws_args[@]}" logs delete-log-group --log-group-name "$log_group" + else + echo "Would delete unmanaged log group: $log_group" + fi + done +done + +if [[ "$execute" != true ]]; then + echo "Dry run complete. Re-run with --execute to delete the unmanaged log groups listed above." +fi diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index a1f20f1b0..ffd0501b0 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -68,6 +68,9 @@ jobs: cache: maven - name: Build locally run: mvn -B -q -Dmaven.test.skip=true install --file pom.xml + - name: Generate SAM template + run: python3 generate-template.py + working-directory: ./examples - name: sam build env: MAVEN_OPTS: -DskipTests=true -Dmaven.test.skip=true @@ -75,6 +78,11 @@ jobs: sam build --debug --parameter-overrides \ 'ParameterKey=Architecture,ParameterValue=x86_64 ParameterKey=JavaVersion,ParameterValue=java${{ matrix.java }} ParameterKey=FunctionNamePrefix,ParameterValue=Java${{ matrix.java }}- ParameterKey=RoleArn,ParameterValue=${{ secrets.TEST_LAMBDA_EXECUTION_ROLE_ARN }}' working-directory: ./examples + - name: Clean up unmanaged Lambda log groups + run: | + # TODO: Remove this one-time migration cleanup after existing e2e stacks adopt managed log groups. + .github/scripts/cleanup_e2e_unmanaged_log_groups.sh --execute \ + --stack-name Java${{ matrix.java }}-JavaSDKCloudBasedIntegrationTestStack - name: sam deploy run: | sam deploy --stack-name Java${{ matrix.java }}-JavaSDKCloudBasedIntegrationTestStack \ diff --git a/.gitignore b/.gitignore index 0f2836009..378d24717 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ buildNumber.properties Thumbs.db # Build artifacts +__pycache__/ *.jar *.war *.ear @@ -40,8 +41,9 @@ Thumbs.db # SAM .aws-sam/ +examples/template.yaml samconfig.toml samconfig.toml.bak # OSS -.flattened-pom.xml \ No newline at end of file +.flattened-pom.xml diff --git a/examples/README.md b/examples/README.md index 9a81ace92..f10eac11d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -33,6 +33,7 @@ The local runner executes in-memory and skips wait durations—ideal for fast it ```bash cd examples mvn clean package +python3 generate-template.py sam build sam deploy --guided ``` @@ -45,8 +46,11 @@ sam deploy The SAM template configures: - `DurableConfig` with `ExecutionTimeout` and `RetentionPeriodInDays` +- CloudWatch log groups for Lambda functions with 7 days of retention - IAM permissions for `lambda:CheckpointDurableExecutions` and `lambda:GetDurableExecutionState` +`template.yaml` is generated from the Java example handlers and is intentionally not checked in. Re-run `python3 generate-template.py` after adding or removing a deployable example handler. + ## Invoke Deployed Functions ```bash @@ -103,3 +107,10 @@ mvn test -Dtest=CloudBasedIntegrationTest \ ```bash sam delete ``` + +If an existing e2e test stack has Lambda-created log groups that predate the managed log group resources, clean them up before redeploying: + +```bash +../.github/scripts/cleanup_e2e_unmanaged_log_groups.sh --stack-name Java17-JavaSDKCloudBasedIntegrationTestStack +../.github/scripts/cleanup_e2e_unmanaged_log_groups.sh --execute --stack-name Java17-JavaSDKCloudBasedIntegrationTestStack +``` diff --git a/examples/build-and-run.sh b/examples/build-and-run.sh index 8d8274150..2417d36a1 100755 --- a/examples/build-and-run.sh +++ b/examples/build-and-run.sh @@ -8,6 +8,7 @@ mvn install -DskipTests # Package example cd examples mvn clean package -DskipTests +python3 generate-template.py # Run SAM local invoke sam local invoke SimpleStepExampleFunction --event event.json --skip-pull-image diff --git a/examples/generate-template.py b/examples/generate-template.py new file mode 100755 index 000000000..82625e4ae --- /dev/null +++ b/examples/generate-template.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import argparse +import re +from dataclasses import dataclass +from pathlib import Path + + +EXAMPLES_DIR = Path(__file__).resolve().parent +SOURCE_ROOT = EXAMPLES_DIR / "src/main/java" +EXAMPLE_PACKAGE_ROOT = SOURCE_ROOT / "software/amazon/lambda/durable/examples" +DEFAULT_OUTPUT = EXAMPLES_DIR / "template.yaml" +TEMPLATE_ANNOTATION = "ExampleTemplate" + + +@dataclass(frozen=True) +class ExampleFunction: + class_name: str + package_name: str + suffix: str + condition: str | None + tracing: bool + + @property + def logical_id(self) -> str: + return f"{self.class_name}Function" + + @property + def handler(self) -> str: + return f"{self.package_name}.{self.class_name}" + + @property + def description(self) -> str: + words = re.sub(r"(? str: + return re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "-", name).lower() + + +def read_package(source: str, path: Path) -> str: + match = re.search(r"^package\s+([\w.]+);", source, flags=re.MULTILINE) + if not match: + raise ValueError(f"Missing package declaration: {path}") + return match.group(1) + + +def is_top_level_durable_handler(source: str, class_name: str) -> bool: + match = re.search(rf"public\s+(?:final\s+)?class\s+{class_name}\b(?P
[^\{{]*)\{{", source, re.DOTALL) + return bool(match and "extends DurableHandler" in match.group("header")) + + +def read_template_annotation(source: str, class_name: str) -> tuple[str | None, bool]: + class_match = re.search(rf"public\s+(?:final\s+)?class\s+{class_name}\b", source) + if not class_match: + return None, False + + prefix = source[: class_match.start()] + matches = list( + re.finditer(rf"@(?:[A-Za-z_][\w.]*\.)?{TEMPLATE_ANNOTATION}\s*(?:\((?P.*?)\))?", prefix, re.DOTALL) + ) + if not matches: + return None, False + + body = matches[-1].group("body") or "" + condition_match = re.search(r'condition\s*=\s*"([^"]+)"', body) + tracing_match = re.search(r"tracing\s*=\s*(true|false)", body) + condition = condition_match.group(1) if condition_match else None + tracing = tracing_match.group(1) == "true" if tracing_match else False + return condition, tracing + + +def discover_examples() -> list[ExampleFunction]: + examples = [] + for path in sorted(EXAMPLE_PACKAGE_ROOT.rglob("*.java")): + source = path.read_text(encoding="utf-8") + class_name = path.stem + if not is_top_level_durable_handler(source, class_name): + continue + + condition, tracing = read_template_annotation(source, class_name) + package_name = read_package(source, path) + examples.append( + ExampleFunction( + class_name=class_name, + package_name=package_name, + suffix=kebab_case(class_name), + condition=condition, + tracing=tracing, + ) + ) + return examples + + +def emit_function(lines: list[str], example: ExampleFunction) -> None: + lines.extend( + [ + f" {example.logical_id}:", + " Type: AWS::Serverless::Function", + ] + ) + if example.condition: + lines.append(f" Condition: {example.condition}") + lines.extend( + [ + " Properties:", + f' FunctionName: !Sub "${{FunctionNamePrefix}}{example.suffix}"', + f' Handler: "{example.handler}"', + " Role: !Ref RoleArn", + ] + ) + if example.tracing: + lines.extend( + [ + " Tracing: Active", + " Layers:", + " - !Sub", + " - arn:aws:lambda:${AWS::Region}:901920570463:layer:aws-otel-java-agent-${AdotArch}-ver-1-32-0:6", + " - AdotArch: amd64", + ] + ) + lines.append("") + + +def emit_log_group(lines: list[str], example: ExampleFunction) -> None: + lines.extend( + [ + f" {example.logical_id}LogGroup:", + " Type: AWS::Logs::LogGroup", + ] + ) + if example.condition: + lines.append(f" Condition: {example.condition}") + lines.extend( + [ + " Properties:", + f' LogGroupName: !Sub "/aws/lambda/${{FunctionNamePrefix}}{example.suffix}"', + " RetentionInDays: 7", + "", + ] + ) + + +def render_template(examples: list[ExampleFunction]) -> str: + lines = [ + "# This file is generated by examples/generate-template.py. Do not edit it by hand.", + 'AWSTemplateFormatVersion: "2010-09-09"', + "Transform: AWS::Serverless-2016-10-31", + "Description: AWS Lambda Durable Execution SDK Examples", + "", + "Parameters:", + " Architecture:", + " Type: String", + " Default: arm64", + " Description: Lambda Function Architecture", + " AllowedValues:", + " - x86_64", + " - arm64", + " JavaVersion:", + " Type: String", + " Default: 'java17'", + " Description: Java runtime version", + " FunctionNamePrefix:", + " Type: String", + " Default: ''", + " Description: Optional prefix for Lambda function names", + " RoleArn:", + " Type: String", + " Description: IAM Role ARN for Lambda function execution", + "", + "Conditions:", + " IsJava21OrLater:", + " !Or", + " - !Equals [!Ref JavaVersion, 'java21']", + " - !Equals [!Ref JavaVersion, 'java25']", + "", + "Globals:", + " Function:", + " Timeout: 900", + " MemorySize: 512", + " Architectures:", + " - !Ref Architecture", + " DurableConfig:", + " ExecutionTimeout: 300", + " RetentionPeriodInDays: 7", + " Runtime: !Ref JavaVersion", + " Environment:", + " Variables:", + " FUNCTION_NAME_PREFIX: !Ref FunctionNamePrefix", + "", + "Resources:", + ] + + for example in examples: + emit_function(lines, example) + emit_log_group(lines, example) + + lines.append("Outputs:") + for index, example in enumerate(examples): + if index: + lines.append("") + lines.extend( + [ + f" {example.logical_id}:", + f" Description: {example.description}", + ] + ) + if example.condition: + lines.append(f" Condition: {example.condition}") + lines.append(f" Value: !GetAtt {example.logical_id}.Arn") + + return "\n".join(lines) + "\n" + + +def main() -> None: + parser = argparse.ArgumentParser(description="Generate the examples SAM template from Java example handlers.") + parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT, help="Path to write the generated template.") + args = parser.parse_args() + + examples = discover_examples() + if not examples: + raise RuntimeError("No DurableHandler examples found") + + args.output.write_text(render_template(examples), encoding="utf-8") + print(f"Generated {args.output} with {len(examples)} Lambda functions.") + + +if __name__ == "__main__": + main() diff --git a/examples/src/main/java/software/amazon/lambda/durable/examples/ExampleTemplate.java b/examples/src/main/java/software/amazon/lambda/durable/examples/ExampleTemplate.java new file mode 100644 index 000000000..898634884 --- /dev/null +++ b/examples/src/main/java/software/amazon/lambda/durable/examples/ExampleTemplate.java @@ -0,0 +1,17 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +package software.amazon.lambda.durable.examples; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Metadata used by {@code generate-template.py} when producing the examples SAM template. */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface ExampleTemplate { + String condition() default ""; + + boolean tracing() default false; +} diff --git a/examples/src/main/java/software/amazon/lambda/durable/examples/otel/OtelXRayStepExample.java b/examples/src/main/java/software/amazon/lambda/durable/examples/otel/OtelXRayStepExample.java index 558360e54..f78b0fda7 100644 --- a/examples/src/main/java/software/amazon/lambda/durable/examples/otel/OtelXRayStepExample.java +++ b/examples/src/main/java/software/amazon/lambda/durable/examples/otel/OtelXRayStepExample.java @@ -8,6 +8,7 @@ import software.amazon.lambda.durable.DurableConfig; import software.amazon.lambda.durable.DurableContext; import software.amazon.lambda.durable.DurableHandler; +import software.amazon.lambda.durable.examples.ExampleTemplate; import software.amazon.lambda.durable.examples.types.GreetingRequest; import software.amazon.lambda.durable.otel.OtelPlugin; @@ -31,6 +32,7 @@ * └── transform attempt 1 * */ +@ExampleTemplate(tracing = true) public class OtelXRayStepExample extends DurableHandler { @Override diff --git a/examples/src/main/java/software/amazon/lambda/durable/examples/otel/OtelXRayWaitExample.java b/examples/src/main/java/software/amazon/lambda/durable/examples/otel/OtelXRayWaitExample.java index c3937e1a2..4ee8e6551 100644 --- a/examples/src/main/java/software/amazon/lambda/durable/examples/otel/OtelXRayWaitExample.java +++ b/examples/src/main/java/software/amazon/lambda/durable/examples/otel/OtelXRayWaitExample.java @@ -9,6 +9,7 @@ import software.amazon.lambda.durable.DurableConfig; import software.amazon.lambda.durable.DurableContext; import software.amazon.lambda.durable.DurableHandler; +import software.amazon.lambda.durable.examples.ExampleTemplate; import software.amazon.lambda.durable.examples.types.GreetingRequest; import software.amazon.lambda.durable.otel.OtelPlugin; @@ -43,6 +44,7 @@ * └── durable.step:after-wait [attempt 1] * */ +@ExampleTemplate(tracing = true) public class OtelXRayWaitExample extends DurableHandler { @Override diff --git a/examples/src/main/java/software/amazon/lambda/durable/examples/vt/ManyAsyncStepsVirtualThreadPoolExample.java b/examples/src/main/java/software/amazon/lambda/durable/examples/vt/ManyAsyncStepsVirtualThreadPoolExample.java index bc8364e1a..5e86729de 100644 --- a/examples/src/main/java/software/amazon/lambda/durable/examples/vt/ManyAsyncStepsVirtualThreadPoolExample.java +++ b/examples/src/main/java/software/amazon/lambda/durable/examples/vt/ManyAsyncStepsVirtualThreadPoolExample.java @@ -10,6 +10,7 @@ import software.amazon.lambda.durable.DurableContext; import software.amazon.lambda.durable.DurableFuture; import software.amazon.lambda.durable.DurableHandler; +import software.amazon.lambda.durable.examples.ExampleTemplate; import software.amazon.lambda.durable.examples.types.ManyAsyncStepsInput; import software.amazon.lambda.durable.examples.types.ManyAsyncStepsOutput; @@ -24,6 +25,7 @@ *
  • All results are collected using {@link DurableFuture#allOf} * */ +@ExampleTemplate(condition = "IsJava21OrLater") public class ManyAsyncStepsVirtualThreadPoolExample extends DurableHandler { @Override diff --git a/examples/template.yaml b/examples/template.yaml deleted file mode 100644 index a39a919cf..000000000 --- a/examples/template.yaml +++ /dev/null @@ -1,617 +0,0 @@ -AWSTemplateFormatVersion: "2010-09-09" -Transform: AWS::Serverless-2016-10-31 -Description: AWS Lambda Durable Execution SDK Examples - -Parameters: - Architecture: - Type: String - Default: arm64 - Description: Lambda Function Architecture - AllowedValues: - - x86_64 - - arm64 - JavaVersion: - Type: String - Default: 'java17' - Description: Java runtime version - FunctionNamePrefix: - Type: String - Default: '' - Description: Optional prefix for Lambda function names - RoleArn: - Type: String - Description: IAM Role ARN for Lambda function execution - -Conditions: - IsJava21OrLater: - !Or - - !Equals [!Ref JavaVersion, 'java21'] - - !Equals [!Ref JavaVersion, 'java25'] - -Globals: - Function: - Timeout: 900 - MemorySize: 512 - Architectures: - - !Ref Architecture - DurableConfig: - ExecutionTimeout: 300 - RetentionPeriodInDays: 7 - Runtime: !Ref JavaVersion - Environment: - Variables: - FUNCTION_NAME_PREFIX: !Ref FunctionNamePrefix - -Resources: - NoopExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - noop-example - Handler: "software.amazon.lambda.durable.examples.general.NoopExample" - Role: !Ref RoleArn - - SimpleStepExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - simple-step-example - Handler: "software.amazon.lambda.durable.examples.step.SimpleStepExample" - Role: !Ref RoleArn - - SimpleInvokeExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - simple-invoke-example - Handler: "software.amazon.lambda.durable.examples.invoke.SimpleInvokeExample" - Role: !Ref RoleArn - - WaitExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - wait-example - Handler: "software.amazon.lambda.durable.examples.wait.WaitExample" - Role: !Ref RoleArn - - RetryExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - retry-example - Handler: "software.amazon.lambda.durable.examples.step.RetryExample" - Role: !Ref RoleArn - - WaitAtLeastExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - wait-at-least-example - Handler: "software.amazon.lambda.durable.examples.wait.WaitAtLeastExample" - Role: !Ref RoleArn - - WaitAtLeastInProcessExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - wait-at-least-in-process-example - Handler: "software.amazon.lambda.durable.examples.wait.WaitAtLeastInProcessExample" - Role: !Ref RoleArn - - RetryInProcessExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - retry-in-process-example - Handler: "software.amazon.lambda.durable.examples.step.RetryInProcessExample" - Role: !Ref RoleArn - - GenericTypesExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - generic-types-example - Handler: "software.amazon.lambda.durable.examples.general.GenericTypesExample" - Role: !Ref RoleArn - - GenericInputOutputExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - generic-input-output-example - Handler: "software.amazon.lambda.durable.examples.general.GenericInputOutputExample" - Role: !Ref RoleArn - - CustomConfigExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - custom-config-example - Handler: "software.amazon.lambda.durable.examples.general.CustomConfigExample" - Role: !Ref RoleArn - - LoggingExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - logging-example - Handler: "software.amazon.lambda.durable.examples.general.LoggingExample" - Role: !Ref RoleArn - - ErrorHandlingExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - error-handling-example - Handler: "software.amazon.lambda.durable.examples.general.ErrorHandlingExample" - Role: !Ref RoleArn - - CallbackExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - callback-example - Handler: "software.amazon.lambda.durable.examples.callback.CallbackExample" - Role: !Ref RoleArn - - ManyAsyncStepsExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - many-async-steps-example - Handler: "software.amazon.lambda.durable.examples.step.ManyAsyncStepsExample" - Role: !Ref RoleArn - - ChildContextExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - child-context-example - Handler: "software.amazon.lambda.durable.examples.child.ChildContextExample" - Role: !Ref RoleArn - - VirtualChildContextExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - virtual-child-context-example - Handler: "software.amazon.lambda.durable.examples.child.VirtualChildContextExample" - Role: !Ref RoleArn - - WaitAsyncExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - wait-async-example - Handler: "software.amazon.lambda.durable.examples.wait.WaitAsyncExample" - Role: !Ref RoleArn - - ManyAsyncChildContextExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - many-async-child-context-example - Handler: "software.amazon.lambda.durable.examples.child.ManyAsyncChildContextExample" - Role: !Ref RoleArn - - SimpleMapExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - simple-map-example - Handler: "software.amazon.lambda.durable.examples.map.SimpleMapExample" - Role: !Ref RoleArn - - CustomShouldCompleteMapExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - custom-should-complete-map-example - Handler: "software.amazon.lambda.durable.examples.map.CustomShouldCompleteMapExample" - Role: !Ref RoleArn - - ComplexMapExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - complex-map-example - Handler: "software.amazon.lambda.durable.examples.map.ComplexMapExample" - Role: !Ref RoleArn - - ComplexFlatMapExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - complex-flat-map-example - Handler: "software.amazon.lambda.durable.examples.map.ComplexFlatMapExample" - Role: !Ref RoleArn - - WaitForConditionExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - wait-for-condition-example - Handler: "software.amazon.lambda.durable.examples.wait.WaitForConditionExample" - Role: !Ref RoleArn - - ConcurrentWaitForConditionExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - concurrent-wait-for-condition-example - Handler: "software.amazon.lambda.durable.examples.wait.ConcurrentWaitForConditionExample" - Role: !Ref RoleArn - - RetryWaitForCallbackExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - retry-wait-for-callback-example - Handler: "software.amazon.lambda.durable.examples.callback.RetryWaitForCallbackExample" - Role: !Ref RoleArn - - WaitForCallbackFailedExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - wait-for-callback-failed-example - Handler: "software.amazon.lambda.durable.examples.callback.WaitForCallbackFailedExample" - Role: !Ref RoleArn - - CustomPollingExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - custom-polling-example - Handler: "software.amazon.lambda.durable.examples.general.CustomPollingExample" - Role: !Ref RoleArn - - PluginExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - plugin-example - Handler: "software.amazon.lambda.durable.examples.general.PluginExample" - Role: !Ref RoleArn - - OtelExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - otel-example - Handler: "software.amazon.lambda.durable.examples.general.OtelExample" - Role: !Ref RoleArn - - OtelXRayStepExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - otel-xray-step-example - Handler: "software.amazon.lambda.durable.examples.otel.OtelXRayStepExample" - Role: !Ref RoleArn - Tracing: Active - Layers: - - !Sub - - arn:aws:lambda:${AWS::Region}:901920570463:layer:aws-otel-java-agent-${AdotArch}-ver-1-32-0:6 - - AdotArch: amd64 - - OtelXRayWaitExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - otel-xray-wait-example - Handler: "software.amazon.lambda.durable.examples.otel.OtelXRayWaitExample" - Role: !Ref RoleArn - Tracing: Active - Layers: - - !Sub - - arn:aws:lambda:${AWS::Region}:901920570463:layer:aws-otel-java-agent-${AdotArch}-ver-1-32-0:6 - - AdotArch: amd64 - - RetryInvokeExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - retry-invoke-example - Handler: "software.amazon.lambda.durable.examples.invoke.RetryInvokeExample" - Role: !Ref RoleArn - - DeserializationFailedMapExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - deserialization-failed-map-example - Handler: "software.amazon.lambda.durable.examples.map.DeserializationFailedMapExample" - Role: !Ref RoleArn - - ParallelExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - parallel-example - Handler: "software.amazon.lambda.durable.examples.parallel.ParallelExample" - Role: !Ref RoleArn - - ParallelFailureToleranceExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - parallel-failure-tolerance-example - Handler: "software.amazon.lambda.durable.examples.parallel.ParallelFailureToleranceExample" - Role: !Ref RoleArn - - ParallelWithWaitExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - parallel-with-wait-example - Handler: "software.amazon.lambda.durable.examples.parallel.ParallelWithWaitExample" - Role: !Ref RoleArn - - DeserializationFailedParallelExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - deserialization-failed-parallel-example - Handler: "software.amazon.lambda.durable.examples.parallel.DeserializationFailedParallelExample" - Role: !Ref RoleArn - - DeserializationFailureExampleFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - deserialization-failure-example - Handler: "software.amazon.lambda.durable.examples.step.DeserializationFailureExample" - Role: !Ref RoleArn - - ManyAsyncStepsVirtualThreadPoolExampleFunction: - Type: AWS::Serverless::Function - Condition: IsJava21OrLater - Properties: - FunctionName: !Join - - '' - - - !Ref FunctionNamePrefix - - many-async-steps-virtual-thread-pool-example - Handler: "software.amazon.lambda.durable.examples.vt.ManyAsyncStepsVirtualThreadPoolExample" - Role: !Ref RoleArn - -Outputs: - NoopExampleFunction: - Description: Noop Example Function ARN - Value: !GetAtt NoopExampleFunction.Arn - - SimpleStepExampleFunction: - Description: Simple Step Example Function ARN - Value: !GetAtt SimpleStepExampleFunction.Arn - - SimpleInvokeExampleFunction: - Description: Simple Invoke Example Function ARN - Value: !GetAtt SimpleInvokeExampleFunction.Arn - - WaitExampleFunction: - Description: Wait Example Function ARN - Value: !GetAtt WaitExampleFunction.Arn - - RetryExampleFunction: - Description: Retry Example Function ARN - Value: !GetAtt RetryExampleFunction.Arn - - WaitAtLeastExampleFunction: - Description: Wait At Least Example Function ARN - Value: !GetAtt WaitAtLeastExampleFunction.Arn - - WaitAtLeastInProcessExampleFunction: - Description: Wait At Least In Process Example Function ARN - Value: !GetAtt WaitAtLeastInProcessExampleFunction.Arn - - RetryInProcessExampleFunction: - Description: Retry In Process Example Function ARN - Value: !GetAtt RetryInProcessExampleFunction.Arn - - GenericTypesExampleFunction: - Description: Generic Types Example Function ARN - Value: !GetAtt GenericTypesExampleFunction.Arn - - GenericInputOutputExampleFunction: - Description: Generic Input Output Example Function ARN - Value: !GetAtt GenericInputOutputExampleFunction.Arn - - CustomConfigExampleFunction: - Description: Custom Config Example Function ARN - Value: !GetAtt CustomConfigExampleFunction.Arn - - LoggingExampleFunction: - Description: Logging Example Function ARN - Value: !GetAtt LoggingExampleFunction.Arn - - ErrorHandlingExampleFunction: - Description: Error Handling Example Function ARN - Value: !GetAtt ErrorHandlingExampleFunction.Arn - - CallbackExampleFunction: - Description: Callback Example Function ARN - Value: !GetAtt CallbackExampleFunction.Arn - - ManyAsyncStepsExampleFunction: - Description: Many Async Steps Example Function ARN - Value: !GetAtt ManyAsyncStepsExampleFunction.Arn - - ChildContextExampleFunction: - Description: Child Context Example Function ARN - Value: !GetAtt ChildContextExampleFunction.Arn - - VirtualChildContextExampleFunction: - Description: Virtual Child Context Example Function ARN - Value: !GetAtt VirtualChildContextExampleFunction.Arn - - WaitAsyncExampleFunction: - Description: Wait Async Example Function ARN - Value: !GetAtt WaitAsyncExampleFunction.Arn - - ManyAsyncChildContextExampleFunction: - Description: Many Async Child Context Example Function ARN - Value: !GetAtt ManyAsyncChildContextExampleFunction.Arn - - SimpleMapExampleFunction: - Description: Simple Map Example Function ARN - Value: !GetAtt SimpleMapExampleFunction.Arn - - CustomShouldCompleteMapExampleFunction: - Description: Custom Should Complete Map Example Function ARN - Value: !GetAtt CustomShouldCompleteMapExampleFunction.Arn - - ComplexMapExampleFunction: - Description: Complex Map Example Function ARN - Value: !GetAtt ComplexMapExampleFunction.Arn - - ComplexFlatMapExampleFunction: - Description: Complex Flat Map Example Function ARN - Value: !GetAtt ComplexFlatMapExampleFunction.Arn - - WaitForConditionExampleFunction: - Description: Wait For Condition Example Function ARN - Value: !GetAtt WaitForConditionExampleFunction.Arn - - ConcurrentWaitForConditionExampleFunction: - Description: Concurrent Wait For Condition Example Function ARN - Value: !GetAtt ConcurrentWaitForConditionExampleFunction.Arn - - RetryWaitForCallbackExampleFunction: - Description: Retry Wait For Callback Example Function ARN - Value: !GetAtt RetryWaitForCallbackExampleFunction.Arn - - WaitForCallbackFailedExampleFunction: - Description: Wait For Callback Failed Example Function ARN - Value: !GetAtt WaitForCallbackFailedExampleFunction.Arn - - CustomPollingExampleFunction: - Description: Custom Polling Example Function ARN - Value: !GetAtt CustomPollingExampleFunction.Arn - - PluginExampleFunction: - Description: Plugin Example Function ARN - Value: !GetAtt PluginExampleFunction.Arn - - OtelExampleFunction: - Description: OTel Example Function ARN - Value: !GetAtt OtelExampleFunction.Arn - - OtelXRayStepExampleFunction: - Description: OTel X-Ray Step Example Function ARN - Value: !GetAtt OtelXRayStepExampleFunction.Arn - - OtelXRayWaitExampleFunction: - Description: OTel X-Ray Wait Example Function ARN - Value: !GetAtt OtelXRayWaitExampleFunction.Arn - - RetryInvokeExampleFunction: - Description: Retry Invoke Example Function ARN - Value: !GetAtt RetryInvokeExampleFunction.Arn - - DeserializationFailedMapExampleFunction: - Description: Deserialization Failed Map Example Function ARN - Value: !GetAtt DeserializationFailedMapExampleFunction.Arn - - ParallelExampleFunction: - Description: Parallel Example Function ARN - Value: !GetAtt ParallelExampleFunction.Arn - - ParallelFailureToleranceExampleFunction: - Description: Parallel Failure Tolerance Example Function ARN - Value: !GetAtt ParallelFailureToleranceExampleFunction.Arn - - ParallelWithWaitExampleFunction: - Description: Parallel With Wait Example Function ARN - Value: !GetAtt ParallelWithWaitExampleFunction.Arn - - DeserializationFailedParallelExampleFunction: - Description: Deserialization Failed Parallel Example Function ARN - Value: !GetAtt DeserializationFailedParallelExampleFunction.Arn - - DeserializationFailureExampleFunction: - Description: Deserialization Failure Example Function ARN - Value: !GetAtt DeserializationFailureExampleFunction.Arn - - ManyAsyncStepsVirtualThreadPoolExampleFunction: - Condition: IsJava21OrLater - Description: Many Async Steps Virtual Thread Pool Example Function ARN - Value: !GetAtt ManyAsyncStepsVirtualThreadPoolExampleFunction.Arn From 955fa44ba7f4ab500ec094e5e30475b28d862c23 Mon Sep 17 00:00:00 2001 From: Frank Chen Date: Fri, 10 Jul 2026 13:33:58 -0700 Subject: [PATCH 2/2] chore: order generated log groups before functions --- examples/generate-template.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/examples/generate-template.py b/examples/generate-template.py index 82625e4ae..75e6cce09 100755 --- a/examples/generate-template.py +++ b/examples/generate-template.py @@ -27,6 +27,10 @@ class ExampleFunction: def logical_id(self) -> str: return f"{self.class_name}Function" + @property + def log_group_logical_id(self) -> str: + return f"{self.logical_id}LogGroup" + @property def handler(self) -> str: return f"{self.package_name}.{self.class_name}" @@ -105,6 +109,7 @@ def emit_function(lines: list[str], example: ExampleFunction) -> None: ) if example.condition: lines.append(f" Condition: {example.condition}") + lines.append(f" DependsOn: {example.log_group_logical_id}") lines.extend( [ " Properties:", @@ -129,7 +134,7 @@ def emit_function(lines: list[str], example: ExampleFunction) -> None: def emit_log_group(lines: list[str], example: ExampleFunction) -> None: lines.extend( [ - f" {example.logical_id}LogGroup:", + f" {example.log_group_logical_id}:", " Type: AWS::Logs::LogGroup", ] ) @@ -196,8 +201,8 @@ def render_template(examples: list[ExampleFunction]) -> str: ] for example in examples: - emit_function(lines, example) emit_log_group(lines, example) + emit_function(lines, example) lines.append("Outputs:") for index, example in enumerate(examples):