Skip to content

Commit 094dbf9

Browse files
authored
feat: add SnapStart support via SnapStartResource trait (#1150)
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 094dbf9

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)