Skip to content

Commit ebef0ad

Browse files
authored
Lambda Worker contrib package (#1995)
1 parent 348ffb8 commit ebef0ad

20 files changed

Lines changed: 1515 additions & 2 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ packages/core-bridge/releases
1111
packages/*/package-lock.json
1212
/sdk-node.iml
1313
*~
14+
.claude/settings.local.json
1415

1516
# One test creates persisted SQLite DBs; they should normally be deleted automatically,
1617
# but may be left behind in some error scenarios.

packages/core-bridge/src/worker.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -868,7 +868,7 @@ mod custom_slot_supplier {
868868
}
869869
Err(err) => {
870870
warn!("Error reserving slot: {err:?}");
871-
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
871+
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
872872
}
873873
}
874874
}

packages/envconfig/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export {
1111
ClientConfig,
1212
ClientConfigProfile,
1313
ClientConfigTLS,
14+
ClientConnectConfig,
1415
LoadClientConfigOptions,
1516
LoadClientProfileOptions,
1617
ClientConfigFromTomlOptions,

packages/lambda-worker/README.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# lambda-worker
2+
3+
A wrapper for running [Temporal](https://temporal.io) workers inside AWS Lambda. A single
4+
`runWorker` call handles the full per-invocation lifecycle: connecting to the Temporal server,
5+
creating a worker with Lambda-tuned defaults, polling for tasks, and gracefully shutting down before
6+
the invocation deadline.
7+
8+
## Quick start
9+
10+
```typescript
11+
// handler.ts
12+
import { runWorker } from '@temporalio/lambda-worker';
13+
import * as activities from './activities';
14+
15+
export const handler = runWorker({ deploymentName: 'my-service', buildId: 'v1.0' }, (config) => {
16+
config.workerOptions.taskQueue = 'my-task-queue';
17+
config.workerOptions.workflowBundle = { code: require('./workflow-bundle.js') };
18+
config.workerOptions.activities = activities;
19+
});
20+
```
21+
22+
Prefer `workflowBundle` (pre-bundled with `bundleWorkflowCode`) over `workflowsPath` to avoid
23+
webpack bundling overhead on Lambda cold starts.
24+
25+
## Configuration
26+
27+
Client connection settings (address, namespace, TLS, API key) are loaded automatically from a TOML
28+
config file and/or environment variables via `@temporalio/envconfig`. The config file is resolved in
29+
order:
30+
31+
1. `TEMPORAL_CONFIG_FILE` env var, if set.
32+
2. `temporal.toml` in `$LAMBDA_TASK_ROOT` (typically `/var/task`).
33+
3. `temporal.toml` in the current working directory.
34+
35+
The file is optional -- if absent, only environment variables are used.
36+
37+
The configure callback receives a `LambdaWorkerConfig` object with fields pre-populated with
38+
Lambda-appropriate defaults. Override any field directly in the callback. The `taskQueue` in
39+
`workerOptions` is pre-populated from the `TEMPORAL_TASK_QUEUE` environment variable if set.
40+
41+
## Lambda-tuned worker defaults
42+
43+
The package applies conservative concurrency limits suited to Lambda's resource constraints:
44+
45+
| Setting | Default |
46+
| -------------------------------------- | ------------------ |
47+
| `maxConcurrentActivityTaskExecutions` | 2 |
48+
| `maxConcurrentWorkflowTaskExecutions` | 10 |
49+
| `maxConcurrentLocalActivityExecutions` | 2 |
50+
| `maxConcurrentNexusTaskExecutions` | 5 |
51+
| `workflowTaskPollerBehavior` | `SimpleMaximum(2)` |
52+
| `activityTaskPollerBehavior` | `SimpleMaximum(1)` |
53+
| `nexusTaskPollerBehavior` | `SimpleMaximum(1)` |
54+
| `shutdownGraceTime` | 5 seconds |
55+
| `maxCachedWorkflows` | 30 |
56+
57+
Worker Deployment Versioning is always enabled. The default versioning behavior is `PINNED`; to
58+
change it, override `workerDeploymentOptions.defaultVersioningBehavior` in the configure callback:
59+
60+
```typescript
61+
config.workerOptions.workerDeploymentOptions = {
62+
defaultVersioningBehavior: 'AUTO_UPGRADE',
63+
};
64+
```
65+
66+
## Logging
67+
68+
The Temporal `Runtime` is installed automatically by `runWorker`. If
69+
[`@aws-lambda-powertools/logger`](https://docs.aws.amazon.com/powertools/typescript/latest/features/logger/)
70+
is installed, the runtime is configured with a `PowertoolsLoggerAdapter` that produces structured
71+
JSON output automatically parsed by CloudWatch Logs. If Powertools is not installed, the SDK's
72+
default human-readable logger is used.
73+
74+
To customize the logger or other runtime options, modify `config.runtimeOptions` in the configure
75+
callback:
76+
77+
```typescript
78+
export const handler = runWorker({ deploymentName: 'my-service', buildId: 'v1.0' }, (config) => {
79+
config.workerOptions.taskQueue = 'my-task-queue';
80+
// Use a custom logger
81+
config.runtimeOptions.logger = myCustomLogger;
82+
// Or configure telemetry
83+
config.runtimeOptions.telemetryOptions = { ... };
84+
});
85+
```
86+
87+
Shutdown signals are disabled by default (`shutdownSignals: []`) since Lambda manages its own
88+
lifecycle.
89+
90+
## Observability
91+
92+
Metrics and tracing are opt-in. The `otel` module provides convenience helpers for
93+
[AWS Distro for OpenTelemetry (ADOT)](https://aws-otel.github.io/docs/getting-started/lambda).
94+
95+
Call `applyDefaults(config)` in your configure callback to:
96+
97+
- Register Temporal SDK interceptors (`@temporalio/interceptors-opentelemetry`) for tracing
98+
Workflow, Activity, and Nexus calls.
99+
- Configure the Temporal Core SDK (Rust) to export its own metrics (task latencies, poll counts,
100+
etc.) via OTLP to the collector.
101+
102+
```typescript
103+
import { runWorker } from '@temporalio/lambda-worker';
104+
import { applyDefaults } from '@temporalio/lambda-worker/otel';
105+
import * as activities from './activities';
106+
107+
export const handler = runWorker({ deploymentName: 'my-service', buildId: 'v1.0' }, (config) => {
108+
applyDefaults(config);
109+
config.workerOptions.taskQueue = 'my-task-queue';
110+
config.workerOptions.workflowBundle = { code: require('./workflow-bundle.js') };
111+
config.workerOptions.activities = activities;
112+
});
113+
```
114+
115+
**Important**: When pre-bundling Workflow code with `bundleWorkflowCode()`, pass `makeOtelPlugin()`
116+
so that Workflow interceptor modules are included in the bundle:
117+
118+
```typescript
119+
import { bundleWorkflowCode } from '@temporalio/worker';
120+
import { makeOtelPlugin } from '@temporalio/lambda-worker/otel';
121+
122+
const { plugin } = makeOtelPlugin();
123+
const { code } = await bundleWorkflowCode({
124+
workflowsPath: require.resolve('./workflows'),
125+
plugins: [plugin],
126+
});
127+
```
128+
129+
### AWS setup
130+
131+
Attach two Lambda layers:
132+
133+
1. **[ADOT JavaScript layer](https://aws-otel.github.io/docs/getting-started/lambda/lambda-js)**
134+
auto-instruments the handler and exports Node.js-side traces (Lambda invocation spans, HTTP
135+
calls, etc.) to X-Ray.
136+
2. **[ADOT Collector layer](https://aws-otel.github.io/docs/getting-started/lambda)**
137+
(`aws-otel-collector-amd64`) — runs the OTel Collector as a Lambda extension, receiving
138+
Temporal Core SDK metrics and SDK trace spans via OTLP gRPC on `localhost:4317`, then
139+
forwarding them to CloudWatch/X-Ray via a custom collector config.
140+
141+
Set these environment variables:
142+
143+
```
144+
AWS_LAMBDA_EXEC_WRAPPER=/opt/otel-instrument
145+
OPENTELEMETRY_COLLECTOR_CONFIG_URI=/var/task/otel-collector-config.yaml
146+
```
147+
148+
`AWS_LAMBDA_EXEC_WRAPPER` enables the JS layer's auto-instrumentation.
149+
`OPENTELEMETRY_COLLECTOR_CONFIG_URI` points the collector at a custom config file bundled in
150+
your deployment package — use this to route metrics to CloudWatch EMF, traces to X-Ray, or any
151+
other supported exporter.
152+
153+
Enable X-Ray active tracing on the Lambda function (required for traces to appear):
154+
155+
```bash
156+
aws lambda update-function-configuration --function-name <function-name> \
157+
--tracing-config Mode=Active
158+
```
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
{
2+
"name": "@temporalio/lambda-worker",
3+
"version": "1.15.0",
4+
"description": "Temporal.io SDK AWS Lambda Worker",
5+
"main": "lib/index.js",
6+
"types": "./lib/index.d.ts",
7+
"exports": {
8+
".": {
9+
"types": "./lib/index.d.ts",
10+
"default": "./lib/index.js"
11+
},
12+
"./otel": {
13+
"types": "./lib/otel.d.ts",
14+
"default": "./lib/otel.js"
15+
},
16+
"./lib/*": {
17+
"types": "./lib/*.d.ts",
18+
"default": "./lib/*.js"
19+
}
20+
},
21+
"scripts": {},
22+
"keywords": [
23+
"temporal",
24+
"aws",
25+
"lambda",
26+
"worker"
27+
],
28+
"author": "Temporal Technologies Inc. <sdk@temporal.io>",
29+
"license": "MIT",
30+
"dependencies": {
31+
"@temporalio/common": "workspace:*",
32+
"@temporalio/envconfig": "workspace:*",
33+
"@temporalio/worker": "workspace:*"
34+
},
35+
"devDependencies": {
36+
"@aws-lambda-powertools/logger": "^2.0.0",
37+
"@types/aws-lambda": "^8.10.145"
38+
},
39+
"peerDependencies": {
40+
"@aws-lambda-powertools/logger": "^2.0.0",
41+
"@temporalio/interceptors-opentelemetry": "workspace:*",
42+
"@opentelemetry/exporter-trace-otlp-grpc": "^0.52.0",
43+
"@opentelemetry/resources": "^1.25.1",
44+
"@opentelemetry/sdk-trace-base": "^1.25.1",
45+
"@types/aws-lambda": "^8.10.145"
46+
},
47+
"peerDependenciesMeta": {
48+
"@aws-lambda-powertools/logger": {
49+
"optional": true
50+
},
51+
"@temporalio/interceptors-opentelemetry": {
52+
"optional": true
53+
},
54+
"@opentelemetry/exporter-trace-otlp-grpc": {
55+
"optional": true
56+
},
57+
"@opentelemetry/resources": {
58+
"optional": true
59+
},
60+
"@opentelemetry/sdk-trace-base": {
61+
"optional": true
62+
},
63+
"@types/aws-lambda": {
64+
"optional": true
65+
}
66+
},
67+
"engines": {
68+
"node": ">= 20.0.0"
69+
},
70+
"bugs": {
71+
"url": "https://github.com/temporalio/sdk-typescript/issues"
72+
},
73+
"repository": {
74+
"type": "git",
75+
"url": "git+https://github.com/temporalio/sdk-typescript.git",
76+
"directory": "packages/lambda-worker"
77+
},
78+
"homepage": "https://github.com/temporalio/sdk-typescript/tree/main/packages/lambda-worker",
79+
"publishConfig": {
80+
"access": "public"
81+
},
82+
"files": [
83+
"src",
84+
"lib"
85+
]
86+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import * as fs from 'node:fs';
2+
import * as path from 'node:path';
3+
import {
4+
type ClientConnectConfig,
5+
type LoadClientProfileOptions,
6+
loadClientConnectConfig,
7+
} from '@temporalio/envconfig';
8+
9+
/**
10+
* Load client connection config with Lambda-aware config file resolution.
11+
*
12+
* Resolution order:
13+
* 1. User-provided `configSource` in options (passthrough)
14+
* 2. `TEMPORAL_CONFIG_FILE` env var (handled by envconfig)
15+
* 3. `$LAMBDA_TASK_ROOT/temporal.toml`
16+
* 4. `process.cwd()/temporal.toml`
17+
* 5. Envconfig default (XDG paths / env-only)
18+
*/
19+
export function loadLambdaClientConnectConfig(options?: Partial<LoadClientProfileOptions>): ClientConnectConfig {
20+
const resolvedOptions: LoadClientProfileOptions = { ...options };
21+
22+
// If the user provided a configSource or TEMPORAL_CONFIG_FILE is set, let envconfig handle it
23+
if (!resolvedOptions.configSource && !process.env['TEMPORAL_CONFIG_FILE']) {
24+
const lambdaConfigPath = findLambdaConfigFile();
25+
if (lambdaConfigPath) {
26+
resolvedOptions.configSource = { path: lambdaConfigPath };
27+
}
28+
}
29+
30+
return loadClientConnectConfig(resolvedOptions);
31+
}
32+
33+
function findLambdaConfigFile(): string | undefined {
34+
const candidates: string[] = [];
35+
36+
const lambdaTaskRoot = process.env['LAMBDA_TASK_ROOT'];
37+
if (lambdaTaskRoot) {
38+
candidates.push(path.join(lambdaTaskRoot, 'temporal.toml'));
39+
}
40+
41+
candidates.push(path.join(process.cwd(), 'temporal.toml'));
42+
43+
for (const candidate of candidates) {
44+
try {
45+
fs.accessSync(candidate, fs.constants.R_OK);
46+
return candidate;
47+
} catch {
48+
// File doesn't exist or isn't readable, try next
49+
}
50+
}
51+
52+
return undefined;
53+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { WorkerOptions } from '@temporalio/worker';
2+
3+
/** Minimum work time in ms — error if less. */
4+
export const MINIMUM_WORK_TIME_MS = 1000;
5+
6+
/** Low work time threshold in ms — warn if less. */
7+
export const WARN_WORK_TIME_MS = 5000;
8+
9+
/**
10+
* Default buffer in ms before the Lambda deadline to begin shutdown.
11+
* Equals the default shutdownGraceTime (5s) + 2s margin.
12+
*/
13+
export const DEFAULT_SHUTDOWN_DEADLINE_BUFFER_MS = 7000;
14+
15+
/**
16+
* Lambda-tuned worker defaults. These are conservative values appropriate for
17+
* Lambda's memory and CPU constraints. Users can override any of these via the
18+
* configure callback.
19+
*/
20+
export const LAMBDA_WORKER_DEFAULTS: Partial<WorkerOptions> = {
21+
maxConcurrentActivityTaskExecutions: 2,
22+
maxConcurrentWorkflowTaskExecutions: 10,
23+
maxConcurrentLocalActivityExecutions: 2,
24+
maxConcurrentNexusTaskExecutions: 5,
25+
shutdownGraceTime: '5s',
26+
maxCachedWorkflows: 30,
27+
workflowTaskPollerBehavior: { type: 'simple-maximum', maximum: 2 },
28+
activityTaskPollerBehavior: { type: 'simple-maximum', maximum: 1 },
29+
nexusTaskPollerBehavior: { type: 'simple-maximum', maximum: 1 },
30+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export { runWorker } from './lambda-worker';
2+
export { PowertoolsLoggerAdapter } from './json-logger';
3+
export type { LambdaWorkerConfig, LambdaHandler, ShutdownHook } from './types';

0 commit comments

Comments
 (0)