Skip to content

Commit db37e17

Browse files
committed
fix(snapstart): disable idle pooling on the Lambda Runtime API client
register_extension_internal built its RAPID client with default pooling, so a connection to the Runtime API could be parked in the idle pool and captured in a SnapStart snapshot. That is the same hazard lambda_runtime handles by calling reset_pool() on its own RAPID client during the restore lifecycle -- but nothing resets or re-establishes this one, and register_default_extension terminates the process with exit(1) when its request fails, so handing out a dead connection after restore would kill the restored environment before it serves anything. Pooling costs nothing to give up here: the client issues exactly two requests, `register` and then the long poll for the first extension event, and the long poll's in-flight connection is unaffected by the idle-pool setting. Disabled unconditionally rather than gated on AWS_LAMBDA_INITIALIZATION_TYPE, since there is no case where reuse helps. Extracted as runtime_api_client so the property is testable; the test observes connection lifetime (dropped vs parked), and I confirmed it fails against a default-pooled client before the change.
1 parent 928bc8c commit db37e17

1 file changed

Lines changed: 40 additions & 1 deletion

File tree

src/lib.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,25 @@ fn build_client(idle_timeout: Duration, pooling: Pooling) -> Client<HttpConnecto
876876
builder.build(HttpConnector::new())
877877
}
878878

879+
/// Builds the client used to talk to the Lambda Runtime API (RAPID) for extension
880+
/// registration.
881+
///
882+
/// Idle pooling is disabled. Under SnapStart, a connection parked here is captured in
883+
/// the snapshot and dead after restore — the same hazard `lambda_runtime` handles by
884+
/// calling `reset_pool()` on its own RAPID client in the restore lifecycle. Nothing
885+
/// resets or re-establishes this one, and [`Adapter::register_default_extension`]
886+
/// terminates the process with `exit(1)` if its request fails, so handing out a dead
887+
/// connection would kill a restored environment before it serves anything.
888+
///
889+
/// Pooling costs nothing to give up here: this client issues exactly two requests —
890+
/// `register`, then the long poll for the first extension event — and the long poll's
891+
/// own in-flight connection is unaffected by the idle-pool setting.
892+
fn runtime_api_client() -> Client<HttpConnector, Body> {
893+
let mut builder = Client::builder(hyper_util::rt::TokioExecutor::new());
894+
builder.pool_max_idle_per_host(0);
895+
builder.build(HttpConnector::new())
896+
}
897+
879898
/// Whether a client may keep idle connections alive for reuse. See [`build_client`].
880899
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
881900
enum Pooling {
@@ -1211,7 +1230,7 @@ impl Adapter<HttpConnector, Body> {
12111230
Some(captured) => captured.clone().unwrap_or_else(|| "127.0.0.1:9001".to_string()),
12121231
None => env::var(ENV_LAMBDA_RUNTIME_API).unwrap_or_else(|_| "127.0.0.1:9001".to_string()),
12131232
};
1214-
let client = Client::builder(hyper_util::rt::TokioExecutor::new()).build(HttpConnector::new());
1233+
let client = runtime_api_client();
12151234

12161235
let register_req = hyper::Request::builder()
12171236
.method(Method::POST)
@@ -1821,6 +1840,26 @@ mod tests {
18211840
);
18221841
}
18231842

1843+
/// The Lambda Runtime API client must not retain an idle connection either.
1844+
///
1845+
/// `register_extension_internal` built a default-pooled client. Under SnapStart any
1846+
/// connection it parks is captured in the snapshot and dead after restore — the
1847+
/// same hazard `lambda_runtime`'s own restore path handles by calling
1848+
/// `reset_pool()` on its RAPID client. Nothing re-establishes or resets this one,
1849+
/// and its failure path is `std::process::exit(1)`, so a reused dead connection
1850+
/// would terminate the restored environment. It also has nothing to gain from
1851+
/// pooling: it makes exactly two requests, `register` and then the long poll for
1852+
/// the first extension event.
1853+
#[tokio::test]
1854+
async fn test_runtime_api_client_does_not_retain_connections() {
1855+
let retained = connection_retained_after_request(&runtime_api_client()).await;
1856+
assert!(
1857+
!retained,
1858+
"the Runtime API client must drop its connection rather than park a socket \
1859+
that a snapshot would capture"
1860+
);
1861+
}
1862+
18241863
/// The pre-snapshot client must still never retain an idle connection, so nothing
18251864
/// dead can be captured in the snapshot and handed out after a restore
18261865
/// (hyper#3810). This also covers a consumer driving the `Service` impl directly,

0 commit comments

Comments
 (0)