Skip to content

Commit 476bef3

Browse files
committed
feat: add SnapStart support via SnapStartResource trait
Implement the SnapStart restore lifecycle so Rust Lambda functions can participate in SnapStart optimizations when deployed as container images with custom base images. Custom logic around the snapshot/restore boundary is expressed by implementing the CRaC-style `SnapStartResource` trait and registering instances on the runtime. The trait methods return `BoxFuture` (no `async-trait` dependency) so the trait stays object-safe as `dyn SnapStartResource`. The runtime drives the lifecycle internally: before_snapshot hooks (LIFO) -> GET /restore/next (blocks until restore) -> rebuild RAPID connection pool -> after_restore hooks (FIFO) Resources use stack ordering: register foundations first; before_snapshot runs in reverse registration order and after_restore in registration order. Error handling: a before-snapshot/`/restore/next` failure is reported to Lambda via POST /init/error; an after_restore failure via POST /restore/error. Both are propagated outward so the runtime exits via the normal Result path (no process::exit, so graceful-shutdown handlers still run). Failures to send the report are logged. When SnapStart is not enabled the hooks are never invoked. Connection-pool reset after restore is non-breaking: rather than mutating the existing `Client`, a new `PooledClient` (with lock-free `OnceLock`-based `reset_pool()`) and a `RuntimeApiClient` trait are added to the api-client crate. `lambda_runtime`'s service layer takes a defaulted generic `C = Client` bounded by the trait and uses `PooledClient` internally; the old `Client` is frozen at its 1.0.3 shape and deprecated. cargo-semver-checks confirms all three crates stay non-breaking (api-client is a 1.1.0 minor bump for the deprecation + additive API). Changes: - lambda-runtime-api-client: add `PooledClient` + `RuntimeApiClient` trait + `ClientBuilder::build_pooled`; deprecate `Client`/`build`; bump to 1.1.0 - lambda-runtime: add `SnapStartResource` trait, Init/Restore error requests, `Runtime::is_snapstart()` and `register_snapstart_resource()`; run the restore lifecycle from run()/run_concurrent(); defaulted-generic service layer using `PooledClient`; re-export `SnapStartResource` and `BoxFuture` - lambda-http: add runtime()/runtime_concurrent() and streaming_runtime() helpers; re-export `SnapStartResource` and `BoxFuture` - lambda-extension: use `PooledClient` - examples: add basic-snapstart and http-snapstart - tests: request serialization, full restore lifecycle, LIFO/FIFO ordering, no-op when not SnapStart, and before/after/restore-next failure paths
1 parent 63c45d4 commit 476bef3

22 files changed

Lines changed: 1505 additions & 91 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "basic-snapstart"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
lambda_runtime = { path = "../../lambda-runtime" }
8+
serde = "1.0.219"
9+
serde_json = "1.0"
10+
tokio = { version = "1", features = ["macros"] }
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Container image for the basic-snapstart example.
2+
#
3+
# SnapStart for functions packaged as OCI images requires a custom base image
4+
# whose runtime implements the restore lifecycle — which `lambda_runtime` now
5+
# does. This image builds the example as the Lambda `bootstrap` on top of the
6+
# AWS-provided base (`provided:al2023`), which supports SnapStart.
7+
#
8+
# Build from the REPOSITORY ROOT (the example uses path dependencies on the
9+
# workspace crates, so the build context must include them):
10+
#
11+
# docker build -f examples/basic-snapstart/Dockerfile -t basic-snapstart .
12+
#
13+
# Then push to ECR and create the function with SnapStart enabled
14+
# (PublishedVersions) and the architecture matching your build host.
15+
16+
# ---- build stage ----
17+
FROM public.ecr.aws/lambda/provided:al2023 AS builder
18+
19+
RUN dnf install -y gcc && \
20+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
21+
ENV PATH="/root/.cargo/bin:${PATH}"
22+
23+
# Copy only the workspace crates this example depends on via path, then the
24+
# example itself: lambda_runtime -> lambda_runtime_api_client.
25+
COPY Cargo.* /build/
26+
COPY lambda-runtime /build/lambda-runtime
27+
COPY lambda-runtime-api-client /build/lambda-runtime-api-client
28+
COPY examples/basic-snapstart /build/examples/basic-snapstart
29+
30+
WORKDIR /build/examples/basic-snapstart
31+
RUN cargo build --release
32+
33+
# ---- runtime stage ----
34+
FROM public.ecr.aws/lambda/provided:al2023
35+
36+
COPY --from=builder /build/examples/basic-snapstart/target/release/basic-snapstart ${LAMBDA_RUNTIME_DIR}/bootstrap
37+
38+
ENTRYPOINT [ "/var/runtime/bootstrap" ]

examples/basic-snapstart/README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# AWS Lambda SnapStart example (lambda_runtime)
2+
3+
This example shows how to use AWS Lambda **SnapStart** with the `lambda_runtime`
4+
crate. It implements the [`SnapStartResource`] trait on an `AppState` and
5+
registers it on the runtime, so the runtime runs the resource's `before_snapshot`
6+
hook before the VM snapshot and its `after_restore` hook after each restore.
7+
8+
SnapStart reduces cold-start latency by snapshotting the initialized execution
9+
environment and restoring from it. Resources created during init (connections,
10+
credentials, unique values) may be invalid after restore — `before_snapshot` /
11+
`after_restore` are where you release and re-establish them. When SnapStart is
12+
not enabled, the hooks are never called and the function behaves normally.
13+
14+
## Why a container image?
15+
16+
SnapStart for functions packaged as OCI images requires a custom base image
17+
whose runtime implements the restore lifecycle — which `lambda_runtime` now does.
18+
This example ships a `Dockerfile` that builds the binary as the Lambda
19+
`bootstrap` on top of `public.ecr.aws/lambda/provided:al2023`, which supports
20+
SnapStart.
21+
22+
## Build & Deploy
23+
24+
Build the image from the **repository root** (the example depends on the
25+
workspace crates via path, so the build context must include them):
26+
27+
```sh
28+
docker build -f examples/basic-snapstart/Dockerfile -t basic-snapstart .
29+
```
30+
31+
Push it to ECR and create the function from the image:
32+
33+
```sh
34+
aws ecr create-repository --repository-name basic-snapstart
35+
docker tag basic-snapstart:latest <ACCOUNT>.dkr.ecr.<REGION>.amazonaws.com/basic-snapstart:latest
36+
docker push <ACCOUNT>.dkr.ecr.<REGION>.amazonaws.com/basic-snapstart:latest
37+
38+
aws lambda create-function \
39+
--function-name basic-snapstart \
40+
--package-type Image \
41+
--code ImageUri=<ACCOUNT>.dkr.ecr.<REGION>.amazonaws.com/basic-snapstart:latest \
42+
--role <YOUR_EXECUTION_ROLE_ARN> \
43+
--snap-start ApplyOn=PublishedVersions
44+
```
45+
46+
SnapStart applies to **published versions**, so publish a version to trigger
47+
snapshot creation, then invoke that version (or an alias pointing at it):
48+
49+
```sh
50+
aws lambda publish-version --function-name basic-snapstart
51+
aws lambda invoke --function-name basic-snapstart:1 --payload '{"name":"world"}' out.json
52+
```
53+
54+
## Architecture
55+
56+
The compiled `bootstrap` is architecture-specific. Build on (or for) the same
57+
architecture as the target function — `provided:al2023` is available for both
58+
`x86_64` and `arm64`.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// This example demonstrates using SnapStart with the lambda_runtime crate.
2+
//
3+
// Implement the `SnapStartResource` trait on the types that hold
4+
// snapshot-sensitive state (connections, credentials, cached values) and
5+
// register them on the runtime. When deployed with SnapStart enabled, the
6+
// runtime will:
7+
// 1. Initialize and create resources (e.g., database connections)
8+
// 2. Run each resource's `before_snapshot` hook (reverse registration order)
9+
// before the VM snapshot
10+
// 3. Call `/restore/next`, which blocks until the VM is restored
11+
// 4. Run each resource's `after_restore` hook (registration order) after restore
12+
// 5. Enter the normal invocation loop
13+
//
14+
// When SnapStart is NOT enabled, the hooks are never called and the runtime
15+
// behaves exactly as before.
16+
17+
use lambda_runtime::{service_fn, tracing, BoxFuture, Error, LambdaEvent, Runtime, SnapStartResource};
18+
use serde::{Deserialize, Serialize};
19+
use std::sync::atomic::{AtomicU64, Ordering};
20+
use std::sync::Arc;
21+
22+
#[derive(Deserialize)]
23+
struct Request {
24+
name: String,
25+
}
26+
27+
#[derive(Serialize)]
28+
struct Response {
29+
message: String,
30+
invocation_count: u64,
31+
}
32+
33+
struct AppState {
34+
counter: AtomicU64,
35+
}
36+
37+
impl AppState {
38+
fn new() -> Self {
39+
Self {
40+
counter: AtomicU64::new(0),
41+
}
42+
}
43+
}
44+
45+
impl SnapStartResource for AppState {
46+
fn before_snapshot(&self) -> BoxFuture<'_, Result<(), Error>> {
47+
Box::pin(async move {
48+
tracing::info!("Releasing resources before snapshot");
49+
self.counter.store(0, Ordering::Relaxed);
50+
Ok(())
51+
})
52+
}
53+
54+
fn after_restore(&self) -> BoxFuture<'_, Result<(), Error>> {
55+
Box::pin(async move {
56+
tracing::info!("Re-establishing resources after restore");
57+
Ok(())
58+
})
59+
}
60+
}
61+
62+
#[tokio::main]
63+
async fn main() -> Result<(), Error> {
64+
tracing::init_default_subscriber();
65+
66+
let state = Arc::new(AppState::new());
67+
68+
let state_ref = state.clone();
69+
let handler = service_fn(move |event: LambdaEvent<Request>| {
70+
let state = state_ref.clone();
71+
async move {
72+
let count = state.counter.fetch_add(1, Ordering::Relaxed) + 1;
73+
Ok::<_, Error>(Response {
74+
message: format!("Hello, {}!", event.payload.name),
75+
invocation_count: count,
76+
})
77+
}
78+
});
79+
80+
Runtime::new(handler)
81+
.register_snapstart_resource(state.clone())
82+
.run()
83+
.await?;
84+
Ok(())
85+
}

examples/http-snapstart/Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "http-snapstart"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
lambda_http = { path = "../../lambda-http" }
8+
tokio = { version = "1", features = ["macros"] }

examples/http-snapstart/Dockerfile

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Container image for the http-snapstart example.
2+
#
3+
# SnapStart for functions packaged as OCI images requires a custom base image
4+
# whose runtime implements the restore lifecycle — which `lambda_runtime` now
5+
# does. This image builds the example as the Lambda `bootstrap` on top of the
6+
# AWS-provided base (`provided:al2023`), which supports SnapStart.
7+
#
8+
# Build from the REPOSITORY ROOT (the example uses path dependencies on the
9+
# workspace crates, so the build context must include them):
10+
#
11+
# docker build -f examples/http-snapstart/Dockerfile -t http-snapstart .
12+
#
13+
# Then push to ECR and create the function with SnapStart enabled
14+
# (PublishedVersions) and the architecture matching your build host.
15+
16+
# ---- build stage ----
17+
FROM public.ecr.aws/lambda/provided:al2023 AS builder
18+
19+
RUN dnf install -y gcc && \
20+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
21+
ENV PATH="/root/.cargo/bin:${PATH}"
22+
23+
# Copy only the workspace crates this example depends on via path, then the
24+
# example itself: lambda_http -> lambda_runtime -> lambda_runtime_api_client,
25+
# plus lambda-events (the aws_lambda_events crate used by lambda_http).
26+
COPY Cargo.* /build/
27+
COPY lambda-http /build/lambda-http
28+
COPY lambda-runtime /build/lambda-runtime
29+
COPY lambda-runtime-api-client /build/lambda-runtime-api-client
30+
COPY lambda-events /build/lambda-events
31+
COPY examples/http-snapstart /build/examples/http-snapstart
32+
33+
WORKDIR /build/examples/http-snapstart
34+
RUN cargo build --release
35+
36+
# ---- runtime stage ----
37+
FROM public.ecr.aws/lambda/provided:al2023
38+
39+
COPY --from=builder /build/examples/http-snapstart/target/release/http-snapstart ${LAMBDA_RUNTIME_DIR}/bootstrap
40+
41+
ENTRYPOINT [ "/var/runtime/bootstrap" ]

examples/http-snapstart/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# AWS Lambda SnapStart example (lambda_http)
2+
3+
This example shows how to use AWS Lambda **SnapStart** with the `lambda_http`
4+
crate. It wraps a shared connection pool in a `SnapStartResource` and registers
5+
it via the [`lambda_http::runtime`] helper, so the runtime drains the pool before
6+
the VM snapshot and reconnects it after each restore. The HTTP handler keeps the
7+
usual `lambda_http` request/response ergonomics.
8+
9+
SnapStart reduces cold-start latency by snapshotting the initialized execution
10+
environment and restoring from it. Resources created during init (connections,
11+
credentials, unique values) may be invalid after restore — `before_snapshot` /
12+
`after_restore` are where you release and re-establish them. When SnapStart is
13+
not enabled, the hooks are never called and the function behaves normally.
14+
15+
If you don't need custom hooks, plain `lambda_http::run(...)` already gets
16+
SnapStart support for free: the runtime calls `/restore/next` and rebuilds its
17+
internal RAPID connection pool on restore without any extra code.
18+
19+
## Why a container image?
20+
21+
SnapStart for functions packaged as OCI images requires a custom base image
22+
whose runtime implements the restore lifecycle — which `lambda_runtime` (used by
23+
`lambda_http`) now does. This example ships a `Dockerfile` that builds the binary
24+
as the Lambda `bootstrap` on top of `public.ecr.aws/lambda/provided:al2023`,
25+
which supports SnapStart.
26+
27+
## Build & Deploy
28+
29+
Build the image from the **repository root** (the example depends on the
30+
workspace crates via path, so the build context must include them):
31+
32+
```sh
33+
docker build -f examples/http-snapstart/Dockerfile -t http-snapstart .
34+
```
35+
36+
Push it to ECR and create the function from the image:
37+
38+
```sh
39+
aws ecr create-repository --repository-name http-snapstart
40+
docker tag http-snapstart:latest <ACCOUNT>.dkr.ecr.<REGION>.amazonaws.com/http-snapstart:latest
41+
docker push <ACCOUNT>.dkr.ecr.<REGION>.amazonaws.com/http-snapstart:latest
42+
43+
aws lambda create-function \
44+
--function-name http-snapstart \
45+
--package-type Image \
46+
--code ImageUri=<ACCOUNT>.dkr.ecr.<REGION>.amazonaws.com/http-snapstart:latest \
47+
--role <YOUR_EXECUTION_ROLE_ARN> \
48+
--snap-start ApplyOn=PublishedVersions
49+
```
50+
51+
SnapStart applies to **published versions**, so publish a version to trigger
52+
snapshot creation, then expose it behind a function URL or API Gateway and invoke
53+
it (e.g. `?name=world`).
54+
55+
## Architecture
56+
57+
The compiled `bootstrap` is architecture-specific. Build on (or for) the same
58+
architecture as the target function — `provided:al2023` is available for both
59+
`x86_64` and `arm64`.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// This example demonstrates using SnapStart with the lambda_http crate.
2+
//
3+
// Use lambda_http::runtime() to get a Runtime on which you can register
4+
// `SnapStartResource`s. This gives you access to the SnapStart lifecycle while
5+
// keeping the lambda_http request/response ergonomics.
6+
//
7+
// For the simple case where no custom hooks are needed, just use
8+
// lambda_http::run() — SnapStart works automatically.
9+
10+
use lambda_http::{
11+
service_fn, tracing, BoxFuture, Body, Error, IntoResponse, Request, RequestExt, Response, SnapStartResource,
12+
};
13+
use std::sync::Arc;
14+
use tokio::sync::RwLock;
15+
16+
struct DbPool {
17+
connected: bool,
18+
}
19+
20+
impl DbPool {
21+
async fn connect() -> Self {
22+
tracing::info!("Establishing database connection pool");
23+
Self { connected: true }
24+
}
25+
}
26+
27+
/// A `SnapStartResource` wrapper around the shared connection pool. The handler
28+
/// and the resource share the same `Arc<RwLock<DbPool>>`, so draining before the
29+
/// snapshot and reconnecting after restore are visible to invocations.
30+
struct PoolResource(Arc<RwLock<DbPool>>);
31+
32+
impl SnapStartResource for PoolResource {
33+
fn before_snapshot(&self) -> BoxFuture<'_, Result<(), Error>> {
34+
Box::pin(async move {
35+
tracing::info!("Draining database connections before snapshot");
36+
self.0.write().await.connected = false;
37+
Ok(())
38+
})
39+
}
40+
41+
fn after_restore(&self) -> BoxFuture<'_, Result<(), Error>> {
42+
Box::pin(async move {
43+
tracing::info!("Re-establishing database connections after restore");
44+
self.0.write().await.connected = true;
45+
Ok(())
46+
})
47+
}
48+
}
49+
50+
#[tokio::main]
51+
async fn main() -> Result<(), Error> {
52+
tracing::init_default_subscriber();
53+
54+
let pool = Arc::new(RwLock::new(DbPool::connect().await));
55+
56+
let pool_ref = pool.clone();
57+
let handler = service_fn(move |req: Request| {
58+
let pool = pool_ref.clone();
59+
async move {
60+
let pool = pool.read().await;
61+
assert!(pool.connected, "Pool should be connected during invocation");
62+
63+
let name = req
64+
.query_string_parameters_ref()
65+
.and_then(|params| params.first("name"))
66+
.unwrap_or("world");
67+
68+
Ok::<Response<Body>, Error>(format!("Hello, {name}!").into_response().await)
69+
}
70+
});
71+
72+
lambda_http::runtime(handler)
73+
.register_snapstart_resource(Arc::new(PoolResource(pool.clone())))
74+
.run()
75+
.await?;
76+
Ok(())
77+
}

lambda-extension/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ http = { workspace = true }
2525
http-body-util = { workspace = true }
2626
hyper = { workspace = true, features = ["http1", "client", "server"] }
2727
hyper-util = { workspace = true }
28-
lambda_runtime_api_client = { version = "1.0.3", path = "../lambda-runtime-api-client" }
28+
lambda_runtime_api_client = { version = "1.1.0", path = "../lambda-runtime-api-client" }
2929
serde = { version = "1", features = ["derive"] }
3030
serde_json = "^1"
3131
tokio = { version = "1.0", features = [

0 commit comments

Comments
 (0)