From 7d084b0a32f8b04c70402abb615f17e8c3c37e2d Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Wed, 10 Jun 2026 18:07:09 -0500 Subject: [PATCH 1/3] Sample for AWS lambda worker --- .gitignore | 1 + README.md | 3 + lambda-worker/README.md | 250 ++++++++++++++++++ lambda-worker/build.gradle | 63 +++++ lambda-worker/deploy-lambda.sh | 34 +++ lambda-worker/extra-setup-steps | 44 +++ ...-role-for-temporal-lambda-invoke-test.yaml | 94 +++++++ lambda-worker/mk-iam-role.sh | 14 + .../otel-collector-config.yaml.sample | 33 +++ .../lambdaworker/GreetingActivities.java | 12 + .../lambdaworker/GreetingActivitiesImpl.java | 20 ++ .../samples/lambdaworker/LambdaFunction.java | 21 ++ .../lambdaworker/LambdaWorkerSample.java | 46 ++++ .../samples/lambdaworker/SampleWorkflow.java | 12 + .../lambdaworker/SampleWorkflowImpl.java | 28 ++ .../samples/lambdaworker/Starter.java | 50 ++++ lambda-worker/src/main/resources/logback.xml | 15 ++ .../lambdaworker/LambdaWorkerSampleTest.java | 62 +++++ .../lambdaworker/SampleWorkflowTest.java | 32 +++ lambda-worker/temporal.toml.sample | 9 + settings.gradle | 8 +- 21 files changed, 850 insertions(+), 1 deletion(-) create mode 100644 lambda-worker/README.md create mode 100644 lambda-worker/build.gradle create mode 100755 lambda-worker/deploy-lambda.sh create mode 100755 lambda-worker/extra-setup-steps create mode 100644 lambda-worker/iam-role-for-temporal-lambda-invoke-test.yaml create mode 100755 lambda-worker/mk-iam-role.sh create mode 100644 lambda-worker/otel-collector-config.yaml.sample create mode 100644 lambda-worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivities.java create mode 100644 lambda-worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivitiesImpl.java create mode 100644 lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java create mode 100644 lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java create mode 100644 lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflow.java create mode 100644 lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflowImpl.java create mode 100644 lambda-worker/src/main/java/io/temporal/samples/lambdaworker/Starter.java create mode 100644 lambda-worker/src/main/resources/logback.xml create mode 100644 lambda-worker/src/test/java/io/temporal/samples/lambdaworker/LambdaWorkerSampleTest.java create mode 100644 lambda-worker/src/test/java/io/temporal/samples/lambdaworker/SampleWorkflowTest.java create mode 100644 lambda-worker/temporal.toml.sample diff --git a/.gitignore b/.gitignore index a1abef91..f587ebda 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ target .DS_Store .idea .gradle +settings.local.gradle **/build/ **/out/ .classpath diff --git a/README.md b/README.md index 589932ec..391330b6 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ It contains the following modules: * [SpringBoot](/springboot): showcases SpringBoot autoconfig integration. * [SpringBoot Basic](/springboot-basic): Minimal sample showing SpringBoot autoconfig integration without any extra external dependencies. * [Spring AI](/springai): demonstrates the Temporal Spring AI integration — durable AI agents with chat models, tools, MCP servers, vector stores, and embeddings. +* [Lambda Worker](/lambda-worker): demonstrates running a Temporal Java Worker inside AWS Lambda. ## Learn more about Temporal and Java SDK @@ -115,6 +116,8 @@ See the README.md file in each main sample directory for cut/paste Gradle comman - [**Environment Configuration**](/core/src/main/java/io/temporal/samples/envconfig): Load client configuration from TOML files with programmatic overrides. +- [**Lambda Worker**](/lambda-worker): Demonstrates running a Temporal Java Worker inside AWS Lambda with Worker Deployment Versioning. + #### API demonstrations - [**Async Untyped Child Workflow**](/core/src/main/java/io/temporal/samples/asyncuntypedchild): Demonstrates how to invoke an untyped child workflow async, that can complete after parent workflow is already completed. diff --git a/lambda-worker/README.md b/lambda-worker/README.md new file mode 100644 index 00000000..44cc4668 --- /dev/null +++ b/lambda-worker/README.md @@ -0,0 +1,250 @@ +# Lambda Worker + +This sample demonstrates a Temporal Java Worker running inside an AWS Lambda function. +It registers a simple greeting Workflow and Activity, configures Worker Deployment +Versioning, and includes helper scripts for packaging the Lambda and configuring Temporal +Cloud invocation. + +For local SDK co-development, create `settings.local.gradle` in the repository root and +add `includeBuild '../sdk-java'`. That file is ignored by Git and makes Gradle use the +in-development `io.temporal:temporal-aws-lambda` SDK add-on before it is published. + +## Prerequisites + +- Java 17+ +- AWS CLI configured with permissions to create Lambda functions, IAM roles, and + CloudFormation stacks +- A Temporal Cloud namespace with Serverless Workers enabled, or a self-hosted Temporal + Service configured for AWS Lambda Serverless Workers +- A Temporal Cloud API key. This walkthrough deploys it as a Lambda environment variable + because these are development-only secrets. + +## Files + +| File | Description | +|------|-------------| +| `src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java` | AWS Lambda handler | +| `src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java` | Shared task queue, deployment version, and worker registrations | +| `src/main/java/io/temporal/samples/lambdaworker/SampleWorkflow*.java` | Sample Workflow interface and implementation | +| `src/main/java/io/temporal/samples/lambdaworker/GreetingActivities*.java` | Sample Activity interface and implementation | +| `src/main/java/io/temporal/samples/lambdaworker/Starter.java` | Local helper that starts a Workflow for the Lambda worker | +| `temporal.toml.sample` | Temporal client connection configuration | +| `otel-collector-config.yaml.sample` | Optional ADOT collector configuration | +| `deploy-lambda.sh` | Builds and uploads the Lambda deployment package | +| `mk-iam-role.sh` | Creates the role Temporal Cloud assumes to invoke Lambda | +| `iam-role-for-temporal-lambda-invoke-test.yaml` | CloudFormation template for the invocation role | +| `extra-setup-steps` | Optional IAM and Lambda settings for OpenTelemetry | + +## Build + +```bash +./gradlew :lambda-worker:test +./gradlew :lambda-worker:shadowJar +``` + +The Lambda handler string is: + +```text +io.temporal.samples.lambdaworker.LambdaFunction::handleRequest +``` + +## Configure Environment + +Set AWS, Temporal, and sample names first. Use unique values if you share the account or +namespace with other developers. + +```bash +export AWS_PROFILE= +export AWS_REGION=us-west-2 +export AWS_DEFAULT_REGION="$AWS_REGION" + +export TEMPORAL_ADDRESS=..tmprl.cloud:7233 +export TEMPORAL_NAMESPACE=. +export TEMPORAL_API_KEY= +export TEMPORAL_TLS=true + +export FUNCTION_NAME=my-temporal-java-worker +export EXECUTION_ROLE_NAME="${FUNCTION_NAME}-exec" +export STACK_NAME="${FUNCTION_NAME}-invoke" +export EXTERNAL_ID="${FUNCTION_NAME}-external-id" + +export DEPLOYMENT_NAME=my-app +export BUILD_ID=build-1 +export TASK_QUEUE=serverless-task-queue-java +export WORKFLOW_PREFIX=serverless-workflow-id-java +``` + +The Lambda worker reads these environment variables: + +```bash +TEMPORAL_ADDRESS +TEMPORAL_NAMESPACE +TEMPORAL_API_KEY +TEMPORAL_TASK_QUEUE +TEMPORAL_WORKER_DEPLOYMENT_NAME +TEMPORAL_WORKER_BUILD_ID +``` + +The local starter also reads `TEMPORAL_TASK_QUEUE` and `TEMPORAL_WORKFLOW_ID_PREFIX`. + +You can also use `temporal.toml.sample` as a starting point for `temporal.toml` and set +`TEMPORAL_CONFIG_FILE=temporal.toml`. + +`TEMPORAL_TASK_QUEUE`, `TEMPORAL_WORKER_DEPLOYMENT_NAME`, +`TEMPORAL_WORKER_BUILD_ID`, and `TEMPORAL_WORKFLOW_ID_PREFIX` are optional. The values +above are the sample defaults. + +## Deploy Lambda + +Create the Lambda execution role: + +```bash +cat > /tmp/temporal-lambda-trust-policy.json <<'JSON' +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +} +JSON + +aws iam create-role \ + --role-name "$EXECUTION_ROLE_NAME" \ + --assume-role-policy-document file:///tmp/temporal-lambda-trust-policy.json \ + --query 'Role.Arn' \ + --output text + +aws iam attach-role-policy \ + --role-name "$EXECUTION_ROLE_NAME" \ + --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + +export EXECUTION_ROLE_ARN="$( + aws iam get-role \ + --role-name "$EXECUTION_ROLE_NAME" \ + --query 'Role.Arn' \ + --output text +)" +``` + +Build the deployment jar and create the Java 17 Lambda function. The jar is small enough +for direct upload in this sample; use S3 if your local artifact grows beyond Lambda's +direct upload limit. + +```bash +./gradlew :lambda-worker:shadowJar + +aws lambda create-function \ + --function-name "$FUNCTION_NAME" \ + --runtime java17 \ + --handler io.temporal.samples.lambdaworker.LambdaFunction::handleRequest \ + --role "$EXECUTION_ROLE_ARN" \ + --zip-file fileb://lambda-worker/build/libs/lambda-worker-1.0.0-all.jar \ + --environment "Variables={TEMPORAL_ADDRESS=$TEMPORAL_ADDRESS,TEMPORAL_NAMESPACE=$TEMPORAL_NAMESPACE,TEMPORAL_API_KEY=$TEMPORAL_API_KEY,TEMPORAL_TASK_QUEUE=$TASK_QUEUE,TEMPORAL_WORKER_DEPLOYMENT_NAME=$DEPLOYMENT_NAME,TEMPORAL_WORKER_BUILD_ID=$BUILD_ID}" \ + --timeout 90 \ + --memory-size 1024 \ + --query 'FunctionArn' \ + --output text + +aws lambda wait function-active --function-name "$FUNCTION_NAME" + +export FUNCTION_ARN="$( + aws lambda get-function \ + --function-name "$FUNCTION_NAME" \ + --query 'Configuration.FunctionArn' \ + --output text +)" +``` + +To update code after the function exists: + +```bash +./lambda-worker/deploy-lambda.sh "$FUNCTION_NAME" +``` + +If direct upload is too large, set `LAMBDA_CODE_S3_BUCKET` and rerun: + +```bash +LAMBDA_CODE_S3_BUCKET= ./lambda-worker/deploy-lambda.sh "$FUNCTION_NAME" +``` + +## Configure Invocation + +Create the IAM role that Temporal Cloud assumes to invoke the Lambda: + +```bash +cd lambda-worker +./mk-iam-role.sh "$STACK_NAME" "$EXTERNAL_ID" "$FUNCTION_ARN" +cd .. + +aws cloudformation wait stack-create-complete --stack-name "$STACK_NAME" + +export INVOCATION_ROLE_ARN="$( + aws cloudformation describe-stacks \ + --stack-name "$STACK_NAME" \ + --query "Stacks[0].Outputs[?OutputKey=='RoleARN'].OutputValue | [0]" \ + --output text +)" +``` + +Create and route the Worker Deployment Version: + +```bash +temporal worker deployment create --name "$DEPLOYMENT_NAME" + +temporal worker deployment create-version \ + --deployment-name "$DEPLOYMENT_NAME" \ + --build-id "$BUILD_ID" \ + --aws-lambda-function-arn "$FUNCTION_ARN" \ + --aws-lambda-assume-role-arn "$INVOCATION_ROLE_ARN" \ + --aws-lambda-assume-role-external-id "$EXTERNAL_ID" + +temporal worker deployment set-current-version \ + --deployment-name "$DEPLOYMENT_NAME" \ + --build-id "$BUILD_ID" \ + --allow-no-pollers \ + --yes +``` + +An async Lambda smoke test returns immediately and should produce worker startup logs: + +```bash +aws lambda invoke \ + --function-name "$FUNCTION_NAME" \ + --invocation-type Event \ + --cli-binary-format raw-in-base64-out \ + --payload '{}' \ + /tmp/lambda-worker-response.json \ + --query 'StatusCode' \ + --output text +``` + +A synchronous invoke can run until the Lambda worker exits near the function timeout. If +you want to wait for that path, set the AWS CLI read timeout higher than the function +timeout. + +## Start Workflow + +After the Worker Deployment Version is current, start the sample Workflow: + +```bash +export TEMPORAL_TASK_QUEUE="$TASK_QUEUE" +export TEMPORAL_WORKFLOW_ID_PREFIX="$WORKFLOW_PREFIX" + +./gradlew -q :lambda-worker:execute \ + -PmainClass=io.temporal.samples.lambdaworker.Starter +``` + +The starter only creates a Workflow Execution. It does not start a local Worker. The +important value is `TEMPORAL_TASK_QUEUE`; it must match the task queue configured on the +Lambda function. + +## Local SDK Development + +For local development of the Workflow and Activity logic, run the unit tests. They use +`TestWorkflowRule` and do not require AWS or a running Temporal Service. diff --git a/lambda-worker/build.gradle b/lambda-worker/build.gradle new file mode 100644 index 00000000..a1658c53 --- /dev/null +++ b/lambda-worker/build.gradle @@ -0,0 +1,63 @@ +dependencies { + implementation "io.temporal:temporal-sdk:$javaSDKVersion" + implementation "io.temporal:temporal-envconfig:$javaSDKVersion" + implementation "io.temporal:temporal-aws-lambda:$javaSDKVersion" + implementation group: 'ch.qos.logback', name: 'logback-classic', version: '1.5.6' + + testImplementation "io.temporal:temporal-testing:$javaSDKVersion" + testImplementation "junit:junit:4.13.2" + testImplementation(platform("org.junit:junit-bom:5.10.3")) + testRuntimeOnly "org.junit.vintage:junit-vintage-engine" + + dependencies { + errorproneJavac('com.google.errorprone:javac:9+181-r4173-1') + errorprone('com.google.errorprone:error_prone_core:2.28.0') + } +} + +tasks.register('execute', JavaExec) { + mainClass = findProperty("mainClass") ?: "" + classpath = sourceSets.main.runtimeClasspath + if (findProperty("args")) { + args findProperty("args").tokenize() + } +} + +tasks.register('shadowJar', Jar) { + archiveBaseName = 'lambda-worker' + archiveClassifier = 'all' + archiveVersion = jarVersion + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + dependsOn configurations.runtimeClasspath + def mergedServicesDir = layout.buildDirectory.dir('generated/mergedServices') + + from sourceSets.main.output + from({ + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + }) { + exclude 'META-INF/services/**' + } + from(mergedServicesDir) + + doFirst { + File servicesOutput = mergedServicesDir.get().asFile + delete servicesOutput + File servicesDir = new File(servicesOutput, 'META-INF/services') + servicesDir.mkdirs() + + Map> services = [:].withDefault { new LinkedHashSet<>() } + configurations.runtimeClasspath.files.findAll { it.isFile() }.each { artifact -> + zipTree(artifact).matching { include 'META-INF/services/*' }.files.each { serviceFile -> + serviceFile.eachLine('UTF-8') { line -> + String service = line.trim() + if (!service.isEmpty() && !service.startsWith('#')) { + services[serviceFile.name].add(service) + } + } + } + } + services.each { name, providers -> + new File(servicesDir, name).text = providers.join(System.lineSeparator()) + System.lineSeparator() + } + } +} diff --git a/lambda-worker/deploy-lambda.sh b/lambda-worker/deploy-lambda.sh new file mode 100755 index 00000000..bcf94a2d --- /dev/null +++ b/lambda-worker/deploy-lambda.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -euo pipefail + +FUNCTION_NAME="${1:?Usage: deploy-lambda.sh }" +MAX_DIRECT_UPLOAD_BYTES=50000000 + +cd "$(dirname "$0")/.." +./gradlew :lambda-worker:shadowJar + +JAR_FILE="$(find lambda-worker/build/libs -name 'lambda-worker-*-all.jar' | head -n 1)" + +if stat -f%z "$JAR_FILE" >/dev/null 2>&1; then + JAR_SIZE="$(stat -f%z "$JAR_FILE")" +else + JAR_SIZE="$(stat -c%s "$JAR_FILE")" +fi + +if [[ -n "${LAMBDA_CODE_S3_BUCKET:-}" ]]; then + S3_KEY="${LAMBDA_CODE_S3_KEY:-lambda-worker/$(basename "$JAR_FILE")}" + aws s3 cp "$JAR_FILE" "s3://$LAMBDA_CODE_S3_BUCKET/$S3_KEY" + aws lambda update-function-code \ + --function-name "$FUNCTION_NAME" \ + --s3-bucket "$LAMBDA_CODE_S3_BUCKET" \ + --s3-key "$S3_KEY" + exit 0 +fi + +if (( JAR_SIZE > MAX_DIRECT_UPLOAD_BYTES )); then + echo "Artifact is ${JAR_SIZE} bytes, which is too large for direct Lambda upload." >&2 + echo "Set LAMBDA_CODE_S3_BUCKET and rerun to upload through S3." >&2 + exit 1 +fi + +aws lambda update-function-code --function-name "$FUNCTION_NAME" --zip-file "fileb://$JAR_FILE" diff --git a/lambda-worker/extra-setup-steps b/lambda-worker/extra-setup-steps new file mode 100755 index 00000000..8c6dd0f8 --- /dev/null +++ b/lambda-worker/extra-setup-steps @@ -0,0 +1,44 @@ +#!/bin/bash +set -euo pipefail + +ROLE_NAME="${1:?Usage: extra-setup-steps }" +FUNCTION_NAME="${2:?Usage: extra-setup-steps }" +REGION="${3:?Usage: extra-setup-steps }" +ACCOUNT_ID="${4:?Usage: extra-setup-steps }" + +aws iam put-role-policy \ + --role-name "$ROLE_NAME" \ + --policy-name ADOT-Telemetry-Permissions \ + --policy-document "{ + \"Version\": \"2012-10-17\", + \"Statement\": [ + { + \"Effect\": \"Allow\", + \"Action\": [ + \"logs:CreateLogGroup\", + \"logs:CreateLogStream\", + \"logs:PutLogEvents\" + ], + \"Resource\": \"arn:aws:logs:${REGION}:${ACCOUNT_ID}:log-group:/aws/lambda/${FUNCTION_NAME}:*\" + }, + { + \"Effect\": \"Allow\", + \"Action\": [ + \"xray:PutTraceSegments\", + \"xray:PutTelemetryRecords\" + ], + \"Resource\": \"*\" + }, + { + \"Effect\": \"Allow\", + \"Action\": [ + \"cloudwatch:PutMetricData\" + ], + \"Resource\": \"*\" + } + ] + }" + +aws lambda update-function-configuration \ + --function-name "$FUNCTION_NAME" \ + --tracing-config Mode=Active diff --git a/lambda-worker/iam-role-for-temporal-lambda-invoke-test.yaml b/lambda-worker/iam-role-for-temporal-lambda-invoke-test.yaml new file mode 100644 index 00000000..2e9054e2 --- /dev/null +++ b/lambda-worker/iam-role-for-temporal-lambda-invoke-test.yaml @@ -0,0 +1,94 @@ +AWSTemplateFormatVersion: "2010-09-09" +Description: Creates an IAM role that Temporal Cloud can assume to invoke Lambda functions for Serverless Workers. + +Parameters: + AssumeRoleExternalId: + Type: String + Description: A string you choose. Use the same value when creating the Worker Deployment Version. + AllowedPattern: "[a-zA-Z0-9_+=,.@-]*" + MinLength: 5 + MaxLength: 45 + + LambdaFunctionARNs: + Type: CommaDelimitedList + Description: Comma-separated list of Lambda function ARNs to invoke. + + RoleName: + Type: String + Default: "Temporal-Cloud-Serverless-Worker" + +Metadata: + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: "Temporal Cloud Configuration" + Parameters: + - AssumeRoleExternalId + - Label: + default: "Lambda Configuration" + Parameters: + - LambdaFunctionARNs + - RoleName + ParameterLabels: + AssumeRoleExternalId: + default: "External ID" + LambdaFunctionARNs: + default: "Lambda Function ARNs" + RoleName: + default: "IAM Role Name" + +Resources: + TemporalCloudServerlessWorker: + Type: AWS::IAM::Role + Properties: + RoleName: !Sub "${RoleName}-${AWS::StackName}" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + AWS: + [ + arn:aws:iam::902542641901:role/wci-lambda-invoke, + arn:aws:iam::160190466495:role/wci-lambda-invoke, + arn:aws:iam::819232936619:role/wci-lambda-invoke, + arn:aws:iam::829909441867:role/wci-lambda-invoke, + arn:aws:iam::354116250941:role/wci-lambda-invoke, + ] + Action: sts:AssumeRole + Condition: + StringEquals: + "sts:ExternalId": [!Ref AssumeRoleExternalId] + Description: The role Temporal Cloud uses to invoke Lambda functions for Serverless Workers. + MaxSessionDuration: 3600 + + TemporalCloudLambdaInvokePermissions: + Type: AWS::IAM::Policy + DependsOn: TemporalCloudServerlessWorker + Properties: + PolicyName: "Temporal-Cloud-Lambda-Invoke-Permissions" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - lambda:InvokeFunction + - lambda:GetFunction + Resource: !Ref LambdaFunctionARNs + Roles: + - !Sub "${RoleName}-${AWS::StackName}" + +Outputs: + RoleARN: + Description: The ARN of the IAM role created for Temporal Cloud. + Value: !GetAtt TemporalCloudServerlessWorker.Arn + Export: + Name: !Sub "${AWS::StackName}-RoleARN" + + RoleName: + Description: The name of the IAM role. + Value: !Ref RoleName + + LambdaFunctionARNs: + Description: The Lambda function ARNs that can be invoked. + Value: !Join [", ", !Ref LambdaFunctionARNs] diff --git a/lambda-worker/mk-iam-role.sh b/lambda-worker/mk-iam-role.sh new file mode 100755 index 00000000..20aeae5d --- /dev/null +++ b/lambda-worker/mk-iam-role.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -euo pipefail + +STACK_NAME="${1:?Usage: mk-iam-role.sh }" +EXTERNAL_ID="${2:?Usage: mk-iam-role.sh }" +LAMBDA_ARN="${3:?Usage: mk-iam-role.sh }" + +aws cloudformation create-stack \ + --stack-name "$STACK_NAME" \ + --template-body file://iam-role-for-temporal-lambda-invoke-test.yaml \ + --parameters \ + ParameterKey=AssumeRoleExternalId,ParameterValue="$EXTERNAL_ID" \ + ParameterKey=LambdaFunctionARNs,ParameterValue="\"$LAMBDA_ARN\"" \ + --capabilities CAPABILITY_NAMED_IAM diff --git a/lambda-worker/otel-collector-config.yaml.sample b/lambda-worker/otel-collector-config.yaml.sample new file mode 100644 index 00000000..790be3d0 --- /dev/null +++ b/lambda-worker/otel-collector-config.yaml.sample @@ -0,0 +1,33 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: "localhost:4317" + http: + endpoint: "localhost:4318" + +exporters: + debug: + awsxray: + region: us-west-2 + awsemf: + namespace: TemporalWorkerMetrics + log_group_name: /aws/lambda/ + region: us-west-2 + dimension_rollup_option: NoDimensionRollup + resource_to_telemetry_conversion: + enabled: true + +service: + pipelines: + traces: + receivers: [otlp] + exporters: [awsxray, debug] + metrics: + receivers: [otlp] + exporters: [awsemf] + telemetry: + logs: + level: debug + metrics: + address: localhost:8888 diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivities.java b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivities.java new file mode 100644 index 00000000..620b7212 --- /dev/null +++ b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivities.java @@ -0,0 +1,12 @@ +package io.temporal.samples.lambdaworker; + +import io.temporal.activity.ActivityInterface; +import io.temporal.activity.ActivityMethod; + +/** Activity interface used by the sample workflow. */ +@ActivityInterface +public interface GreetingActivities { + + @ActivityMethod + String createGreeting(String name); +} diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivitiesImpl.java b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivitiesImpl.java new file mode 100644 index 00000000..07349515 --- /dev/null +++ b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/GreetingActivitiesImpl.java @@ -0,0 +1,20 @@ +package io.temporal.samples.lambdaworker; + +import io.temporal.activity.Activity; +import io.temporal.activity.ActivityInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Activity implementation that returns a simple greeting. */ +public class GreetingActivitiesImpl implements GreetingActivities { + + private static final Logger logger = LoggerFactory.getLogger(GreetingActivitiesImpl.class); + + @Override + public String createGreeting(String name) { + ActivityInfo info = Activity.getExecutionContext().getInfo(); + logger.info( + "Running activity {} for workflow {}", info.getActivityType(), info.getWorkflowId()); + return "Hello, " + name + "!"; + } +} diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java new file mode 100644 index 00000000..547a6d71 --- /dev/null +++ b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java @@ -0,0 +1,21 @@ +package io.temporal.samples.lambdaworker; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import io.temporal.aws.lambda.LambdaWorker; +import io.temporal.common.WorkerDeploymentVersion; + +/** AWS Lambda entry point for the Temporal worker. */ +public class LambdaFunction implements RequestHandler { + + private static final RequestHandler WORKER = + LambdaWorker.run( + new WorkerDeploymentVersion( + LambdaWorkerSample.deploymentName(), LambdaWorkerSample.buildId()), + LambdaWorkerSample::configure); + + @Override + public Void handleRequest(Object input, Context context) { + return WORKER.handleRequest(input, context); + } +} diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java new file mode 100644 index 00000000..84868d0b --- /dev/null +++ b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java @@ -0,0 +1,46 @@ +package io.temporal.samples.lambdaworker; + +import io.temporal.aws.lambda.LambdaWorkerOptions; + +/** Shared constants and worker registration for the Lambda Worker sample. */ +public final class LambdaWorkerSample { + + public static final String TASK_QUEUE_ENV = "TEMPORAL_TASK_QUEUE"; + public static final String DEPLOYMENT_NAME_ENV = "TEMPORAL_WORKER_DEPLOYMENT_NAME"; + public static final String BUILD_ID_ENV = "TEMPORAL_WORKER_BUILD_ID"; + public static final String WORKFLOW_ID_PREFIX_ENV = "TEMPORAL_WORKFLOW_ID_PREFIX"; + + public static final String DEFAULT_TASK_QUEUE = "serverless-task-queue-java"; + public static final String DEFAULT_WORKFLOW_ID_PREFIX = "serverless-workflow-id-java"; + public static final String DEFAULT_DEPLOYMENT_NAME = "my-app"; + public static final String DEFAULT_BUILD_ID = "build-1"; + + public static void configure(LambdaWorkerOptions options) { + options.setTaskQueue(taskQueue()); + options.registerWorkflowImplementationTypes(SampleWorkflowImpl.class); + options.registerActivitiesImplementations(new GreetingActivitiesImpl()); + } + + public static String taskQueue() { + return envOrDefault(TASK_QUEUE_ENV, DEFAULT_TASK_QUEUE); + } + + public static String workflowIdPrefix() { + return envOrDefault(WORKFLOW_ID_PREFIX_ENV, DEFAULT_WORKFLOW_ID_PREFIX); + } + + public static String deploymentName() { + return envOrDefault(DEPLOYMENT_NAME_ENV, DEFAULT_DEPLOYMENT_NAME); + } + + public static String buildId() { + return envOrDefault(BUILD_ID_ENV, DEFAULT_BUILD_ID); + } + + private static String envOrDefault(String name, String defaultValue) { + String value = System.getenv(name); + return value == null || value.isBlank() ? defaultValue : value; + } + + private LambdaWorkerSample() {} +} diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflow.java b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflow.java new file mode 100644 index 00000000..25aa58f8 --- /dev/null +++ b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflow.java @@ -0,0 +1,12 @@ +package io.temporal.samples.lambdaworker; + +import io.temporal.workflow.WorkflowInterface; +import io.temporal.workflow.WorkflowMethod; + +/** Sample workflow run by the Lambda worker. */ +@WorkflowInterface +public interface SampleWorkflow { + + @WorkflowMethod + String getGreeting(String name); +} diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflowImpl.java b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflowImpl.java new file mode 100644 index 00000000..0f6e7cbb --- /dev/null +++ b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflowImpl.java @@ -0,0 +1,28 @@ +package io.temporal.samples.lambdaworker; + +import io.temporal.activity.ActivityOptions; +import io.temporal.common.VersioningBehavior; +import io.temporal.workflow.Workflow; +import io.temporal.workflow.WorkflowVersioningBehavior; +import java.time.Duration; +import org.slf4j.Logger; + +/** Workflow implementation that executes a greeting Activity. */ +public class SampleWorkflowImpl implements SampleWorkflow { + + private static final Logger logger = Workflow.getLogger(SampleWorkflowImpl.class); + + private final GreetingActivities activities = + Workflow.newActivityStub( + GreetingActivities.class, + ActivityOptions.newBuilder().setStartToCloseTimeout(Duration.ofSeconds(10)).build()); + + @Override + @WorkflowVersioningBehavior(VersioningBehavior.PINNED) + public String getGreeting(String name) { + logger.info("SampleWorkflow started for {}", name); + String result = activities.createGreeting(name); + logger.info("SampleWorkflow completed with {}", result); + return result; + } +} diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/Starter.java b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/Starter.java new file mode 100644 index 00000000..c8e46a4b --- /dev/null +++ b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/Starter.java @@ -0,0 +1,50 @@ +package io.temporal.samples.lambdaworker; + +import io.temporal.api.common.v1.WorkflowExecution; +import io.temporal.client.WorkflowClient; +import io.temporal.client.WorkflowOptions; +import io.temporal.client.WorkflowStub; +import io.temporal.envconfig.ClientConfigProfile; +import io.temporal.serviceclient.WorkflowServiceStubs; +import java.io.IOException; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** Local helper that starts a Workflow execution for the Lambda worker to process. */ +public class Starter { + + public static void main(String[] args) { + String name = args.length > 0 ? String.join(" ", args) : "Serverless Lambda Worker!"; + + WorkflowServiceStubs service = null; + try { + ClientConfigProfile profile = ClientConfigProfile.load(); + service = WorkflowServiceStubs.newServiceStubs(profile.toWorkflowServiceStubsOptions()); + WorkflowClient client = + WorkflowClient.newInstance(service, profile.toWorkflowClientOptions()); + + SampleWorkflow workflow = + client.newWorkflowStub( + SampleWorkflow.class, + WorkflowOptions.newBuilder() + .setWorkflowId(LambdaWorkerSample.workflowIdPrefix() + "-" + UUID.randomUUID()) + .setTaskQueue(LambdaWorkerSample.taskQueue()) + .build()); + + WorkflowExecution execution = WorkflowClient.start(workflow::getGreeting, name); + System.out.printf( + "Started workflow WorkflowID=%s RunID=%s%n", + execution.getWorkflowId(), execution.getRunId()); + + String result = WorkflowStub.fromTyped(workflow).getResult(String.class); + System.out.println("Workflow result: " + result); + } catch (IOException e) { + throw new RuntimeException("Failed to load Temporal client configuration", e); + } finally { + if (service != null) { + service.shutdown(); + service.awaitTermination(10, TimeUnit.SECONDS); + } + } + } +} diff --git a/lambda-worker/src/main/resources/logback.xml b/lambda-worker/src/main/resources/logback.xml new file mode 100644 index 00000000..7c5faa44 --- /dev/null +++ b/lambda-worker/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + %d{HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n + + + + + + + + + + + diff --git a/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/LambdaWorkerSampleTest.java b/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/LambdaWorkerSampleTest.java new file mode 100644 index 00000000..a4ca9927 --- /dev/null +++ b/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/LambdaWorkerSampleTest.java @@ -0,0 +1,62 @@ +package io.temporal.samples.lambdaworker; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.amazonaws.services.lambda.runtime.RequestHandler; +import io.temporal.aws.lambda.LambdaWorker; +import io.temporal.aws.lambda.LambdaWorkerOptions; +import io.temporal.common.WorkerDeploymentVersion; +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; + +/** Tests Lambda worker registration without AWS. */ +public class LambdaWorkerSampleTest { + + @Test + public void configureSetsTaskQueueAndRegistrations() throws Exception { + LambdaWorkerOptions options = LambdaWorkerOptions.fromEnvironment(baseEnv()); + + LambdaWorkerSample.configure(options); + + assertEquals(LambdaWorkerSample.DEFAULT_TASK_QUEUE, options.getTaskQueue()); + assertEquals(2, registrations(options).size()); + + RequestHandler handler = + LambdaWorker.newHandler( + new WorkerDeploymentVersion( + LambdaWorkerSample.DEFAULT_DEPLOYMENT_NAME, LambdaWorkerSample.DEFAULT_BUILD_ID), + options); + assertNotNull(handler); + } + + @Test + public void configureUsesDefaultTaskQueueWhenNoProcessEnvironmentOverride() throws Exception { + Map env = baseEnv(); + env.put(LambdaWorkerOptions.TEMPORAL_TASK_QUEUE, "from-env"); + LambdaWorkerOptions options = LambdaWorkerOptions.fromEnvironment(env); + + LambdaWorkerSample.configure(options); + + assertEquals(LambdaWorkerSample.DEFAULT_TASK_QUEUE, options.getTaskQueue()); + assertEquals(2, registrations(options).size()); + } + + private static Map baseEnv() { + Map env = new HashMap<>(); + env.put(LambdaWorkerOptions.TEMPORAL_CONFIG_FILE, "/nonexistent/temporal.toml"); + return env; + } + + private static List registrations(LambdaWorkerOptions options) throws Exception { + Field registrations = LambdaWorkerOptions.class.getDeclaredField("registrations"); + registrations.setAccessible(true); + Object value = registrations.get(options); + assertTrue(value instanceof List); + return (List) value; + } +} diff --git a/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/SampleWorkflowTest.java b/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/SampleWorkflowTest.java new file mode 100644 index 00000000..d4d2367b --- /dev/null +++ b/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/SampleWorkflowTest.java @@ -0,0 +1,32 @@ +package io.temporal.samples.lambdaworker; + +import static org.junit.Assert.assertEquals; + +import io.temporal.client.WorkflowOptions; +import io.temporal.testing.TestWorkflowRule; +import org.junit.Rule; +import org.junit.Test; + +/** Unit test for the sample Workflow and Activity. */ +public class SampleWorkflowTest { + + @Rule + public TestWorkflowRule testWorkflowRule = + TestWorkflowRule.newBuilder() + .setWorkflowTypes(SampleWorkflowImpl.class) + .setActivityImplementations(new GreetingActivitiesImpl()) + .build(); + + @Test + public void workflowReturnsGreeting() { + SampleWorkflow workflow = + testWorkflowRule + .getWorkflowClient() + .newWorkflowStub( + SampleWorkflow.class, + WorkflowOptions.newBuilder().setTaskQueue(testWorkflowRule.getTaskQueue()).build()); + + assertEquals( + "Hello, Serverless Lambda Worker!!", workflow.getGreeting("Serverless Lambda Worker!")); + } +} diff --git a/lambda-worker/temporal.toml.sample b/lambda-worker/temporal.toml.sample new file mode 100644 index 00000000..0117d5cf --- /dev/null +++ b/lambda-worker/temporal.toml.sample @@ -0,0 +1,9 @@ +[profile.default] +address = "..tmprl.cloud:7233" +namespace = "." +api_key = "" + +# For mTLS instead of API key auth, remove api_key and uncomment: +# [profile.default.tls] +# client_cert_path = "client.pem" +# client_key_path = "client.key" diff --git a/settings.gradle b/settings.gradle index b99ad281..c6756a18 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,10 @@ rootProject.name = 'temporal-java-samples' + +def localSettings = file('settings.local.gradle') +if (localSettings.exists()) { + apply from: localSettings +} + include 'core' include 'springai:basic' include 'springai:mcp' @@ -6,4 +12,4 @@ include 'springai:multimodel' include 'springai:rag' include 'springboot' include 'springboot-basic' - +include 'lambda-worker' From 19de5b6665a89e09f402c847a5c9d892fedf1876 Mon Sep 17 00:00:00 2001 From: Edward Amsden Date: Mon, 15 Jun 2026 17:25:13 -0500 Subject: [PATCH 2/3] Use builder for immutable LambdaWorkerOptions --- .../samples/lambdaworker/LambdaWorkerSample.java | 8 ++++---- .../samples/lambdaworker/LambdaWorkerSampleTest.java | 10 ++++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java index 84868d0b..77c3282a 100644 --- a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java +++ b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java @@ -15,10 +15,10 @@ public final class LambdaWorkerSample { public static final String DEFAULT_DEPLOYMENT_NAME = "my-app"; public static final String DEFAULT_BUILD_ID = "build-1"; - public static void configure(LambdaWorkerOptions options) { - options.setTaskQueue(taskQueue()); - options.registerWorkflowImplementationTypes(SampleWorkflowImpl.class); - options.registerActivitiesImplementations(new GreetingActivitiesImpl()); + public static void configure(LambdaWorkerOptions.Builder builder) { + builder.setTaskQueue(taskQueue()); + builder.registerWorkflowImplementationTypes(SampleWorkflowImpl.class); + builder.registerActivitiesImplementations(new GreetingActivitiesImpl()); } public static String taskQueue() { diff --git a/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/LambdaWorkerSampleTest.java b/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/LambdaWorkerSampleTest.java index a4ca9927..b1c9a21b 100644 --- a/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/LambdaWorkerSampleTest.java +++ b/lambda-worker/src/test/java/io/temporal/samples/lambdaworker/LambdaWorkerSampleTest.java @@ -19,9 +19,10 @@ public class LambdaWorkerSampleTest { @Test public void configureSetsTaskQueueAndRegistrations() throws Exception { - LambdaWorkerOptions options = LambdaWorkerOptions.fromEnvironment(baseEnv()); + LambdaWorkerOptions.Builder builder = LambdaWorkerOptions.newBuilderFromEnvironment(baseEnv()); - LambdaWorkerSample.configure(options); + LambdaWorkerSample.configure(builder); + LambdaWorkerOptions options = builder.build(); assertEquals(LambdaWorkerSample.DEFAULT_TASK_QUEUE, options.getTaskQueue()); assertEquals(2, registrations(options).size()); @@ -38,9 +39,10 @@ public void configureSetsTaskQueueAndRegistrations() throws Exception { public void configureUsesDefaultTaskQueueWhenNoProcessEnvironmentOverride() throws Exception { Map env = baseEnv(); env.put(LambdaWorkerOptions.TEMPORAL_TASK_QUEUE, "from-env"); - LambdaWorkerOptions options = LambdaWorkerOptions.fromEnvironment(env); + LambdaWorkerOptions.Builder builder = LambdaWorkerOptions.newBuilderFromEnvironment(env); - LambdaWorkerSample.configure(options); + LambdaWorkerSample.configure(builder); + LambdaWorkerOptions options = builder.build(); assertEquals(LambdaWorkerSample.DEFAULT_TASK_QUEUE, options.getTaskQueue()); assertEquals(2, registrations(options).size()); From df8ee7cc2664bdc70fdcb3a7abe758447a224712 Mon Sep 17 00:00:00 2001 From: Lenny Chen Date: Tue, 16 Jun 2026 15:32:47 -0700 Subject: [PATCH 3/3] Add snipsync markers for docs --- lambda-worker/otel-collector-config.yaml.sample | 2 ++ .../java/io/temporal/samples/lambdaworker/LambdaFunction.java | 2 ++ .../io/temporal/samples/lambdaworker/LambdaWorkerSample.java | 2 ++ .../io/temporal/samples/lambdaworker/SampleWorkflowImpl.java | 2 ++ 4 files changed, 8 insertions(+) diff --git a/lambda-worker/otel-collector-config.yaml.sample b/lambda-worker/otel-collector-config.yaml.sample index 790be3d0..f1c15840 100644 --- a/lambda-worker/otel-collector-config.yaml.sample +++ b/lambda-worker/otel-collector-config.yaml.sample @@ -1,3 +1,4 @@ +# @@@SNIPSTART java-lambda-worker-otel-collector-config receivers: otlp: protocols: @@ -31,3 +32,4 @@ service: level: debug metrics: address: localhost:8888 +# @@@SNIPEND diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java index 547a6d71..acba5bd9 100644 --- a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java +++ b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaFunction.java @@ -1,3 +1,4 @@ +// @@@SNIPSTART java-lambda-worker package io.temporal.samples.lambdaworker; import com.amazonaws.services.lambda.runtime.Context; @@ -19,3 +20,4 @@ public Void handleRequest(Object input, Context context) { return WORKER.handleRequest(input, context); } } +// @@@SNIPEND diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java index 77c3282a..a7898a8c 100644 --- a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java +++ b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/LambdaWorkerSample.java @@ -1,3 +1,4 @@ +// @@@SNIPSTART java-lambda-worker-config package io.temporal.samples.lambdaworker; import io.temporal.aws.lambda.LambdaWorkerOptions; @@ -44,3 +45,4 @@ private static String envOrDefault(String name, String defaultValue) { private LambdaWorkerSample() {} } +// @@@SNIPEND diff --git a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflowImpl.java b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflowImpl.java index 0f6e7cbb..0cfeef1b 100644 --- a/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflowImpl.java +++ b/lambda-worker/src/main/java/io/temporal/samples/lambdaworker/SampleWorkflowImpl.java @@ -1,3 +1,4 @@ +// @@@SNIPSTART java-lambda-worker-workflow package io.temporal.samples.lambdaworker; import io.temporal.activity.ActivityOptions; @@ -26,3 +27,4 @@ public String getGreeting(String name) { return result; } } +// @@@SNIPEND