Skip to content

Commit cfcc875

Browse files
committed
feat: fail initialization when a configured sync-init readiness timeout expires
check_init_health now returns Result; a set AWS_LWA_READINESS_CHECK_TIMEOUT_SECONDS that expires during synchronous cold-start init fails init (refuses to serve) instead of admitting traffic to a never-ready app. Unset stays unbounded/non-fatal; async_init keeps its own non-fatal ~9.8s bound.
1 parent 76f17ba commit cfcc875

4 files changed

Lines changed: 79 additions & 14 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@
2020
- Add `AWS_LWA_READINESS_CHECK_TIMEOUT_SECONDS` to bound the readiness check
2121
(fractional seconds allowed, e.g. `0.5`), applied to both the initial cold-start
2222
readiness wait and the
23-
post-SnapStart-restore readiness check. When unset (the default) the wait is
23+
post-SnapStart-restore readiness check. When set and the app does not become
24+
ready within it, the adapter **refuses to serve**: cold-start init fails (the
25+
runtime never starts) and a restore fails, rather than admitting traffic to an
26+
app that never reported ready. When unset (the default) the wait is
2427
**unbounded**, matching the previous behavior, so existing slow-cold-start apps
2528
are unaffected unless they opt in. The `async_init` initial-readiness path keeps
26-
its own fixed ~9.8s bound and is not affected by this variable.
29+
its own fixed ~9.8s bound (non-fatal) and is not affected by this variable.
2730

2831
### Bug Fixes
2932

‎src/lib.rs‎

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
//! let mut adapter = Adapter::new(&options)?;
3434
//!
3535
//! adapter.register_default_extension();
36-
//! adapter.check_init_health().await;
36+
//! adapter.check_init_health().await?;
3737
//! adapter.run().await
3838
//! })
3939
//! }
@@ -714,7 +714,7 @@ fn matches_hook_path(configured: &Option<String>, request_path: &str) -> bool {
714714
/// let mut adapter = Adapter::new(&options)?;
715715
///
716716
/// adapter.register_default_extension();
717-
/// adapter.check_init_health().await;
717+
/// adapter.check_init_health().await?;
718718
/// adapter.run().await
719719
/// # }
720720
/// ```
@@ -969,7 +969,17 @@ impl Adapter<HttpConnector, Body> {
969969
/// - Allow the application to continue booting in the background
970970
///
971971
/// The first request will re-check readiness if the application wasn't ready
972-
/// during initialization.
972+
/// during initialization. The async path always returns `Ok`.
973+
///
974+
/// # Synchronous Initialization
975+
///
976+
/// Without `async_init`, this waits for the app to report ready before the
977+
/// Lambda runtime starts serving. If `readiness_check_timeout`
978+
/// (`AWS_LWA_READINESS_CHECK_TIMEOUT_SECONDS`) is set and the app does not
979+
/// become ready within it, this returns `Err`: init fails and no traffic is
980+
/// served against an app that never came up. When the timeout is unset the
981+
/// wait is unbounded and this returns `Ok` once the check completes
982+
/// (historical behavior).
973983
///
974984
/// # Examples
975985
///
@@ -979,25 +989,38 @@ impl Adapter<HttpConnector, Body> {
979989
/// # async fn example() -> Result<(), lambda_web_adapter::Error> {
980990
/// let options = AdapterOptions::default();
981991
/// let mut adapter = Adapter::new(&options)?;
982-
/// adapter.check_init_health().await;
992+
/// adapter.check_init_health().await?;
983993
/// # Ok(())
984994
/// # }
985995
/// ```
986-
pub async fn check_init_health(&mut self) {
996+
pub async fn check_init_health(&mut self) -> Result<(), Error> {
987997
let ready_at_init = if self.async_init {
988998
// async_init keeps its own fixed bound, independent of
989999
// readiness_check_timeout (see AdapterOptions::readiness_check_timeout).
1000+
// A timeout here is non-fatal: the app keeps booting and the first
1001+
// request re-checks readiness.
9901002
timeout(Duration::from_secs_f32(9.8), self.check_readiness())
9911003
.await
9921004
.unwrap_or_default()
9931005
} else if let Some(t) = self.readiness_check_timeout {
994-
// Bound the sync-init readiness wait when configured; unset = unbounded
995-
// (historical behavior), so an unset value never fails a slow cold start.
996-
timeout(t, self.check_readiness()).await.unwrap_or_default()
1006+
// Bound the sync-init readiness wait when configured. On expiry, refuse
1007+
// to serve: fail init rather than admit traffic to an app that never
1008+
// reported ready — this is the point of configuring the bound.
1009+
match timeout(t, self.check_readiness()).await {
1010+
Ok(ready) => ready,
1011+
Err(_) => {
1012+
return Err(Error::from(format!(
1013+
"web application did not become ready within {t:?} \
1014+
(AWS_LWA_READINESS_CHECK_TIMEOUT_SECONDS); failing initialization"
1015+
)));
1016+
}
1017+
}
9971018
} else {
1019+
// Unset: unbounded wait, non-fatal result (historical behavior).
9981020
self.check_readiness().await
9991021
};
10001022
self.ready_at_init.store(ready_at_init, Ordering::SeqCst);
1023+
Ok(())
10011024
}
10021025

10031026
/// Performs a single readiness check against the configured endpoint.
@@ -1512,6 +1535,36 @@ mod tests {
15121535
healthcheck.assert();
15131536
}
15141537

1538+
#[tokio::test]
1539+
async fn test_check_init_health_fails_when_sync_init_readiness_timeout_expires() {
1540+
// App server that never reports ready (always 500) so the readiness
1541+
// retry loop runs until the configured timeout fires.
1542+
let app_server = MockServer::start();
1543+
app_server.mock(|when, then| {
1544+
when.method(GET).path("/healthcheck");
1545+
then.status(500).body("nope");
1546+
});
1547+
1548+
// Sync init (async_init defaults to false) with a short configured bound.
1549+
let options = AdapterOptions {
1550+
host: app_server.host(),
1551+
port: app_server.port().to_string(),
1552+
readiness_check_port: app_server.port().to_string(),
1553+
readiness_check_path: "/healthcheck".to_string(),
1554+
readiness_check_timeout: Some(Duration::from_millis(100)),
1555+
..Default::default()
1556+
};
1557+
1558+
let mut adapter = Adapter::new(&options).expect("Failed to create adapter");
1559+
1560+
// Refuse to serve: a configured timeout that expires fails initialization.
1561+
let result = adapter.check_init_health().await;
1562+
assert!(
1563+
result.is_err(),
1564+
"sync-init readiness timeout should fail init, got {result:?}"
1565+
);
1566+
}
1567+
15151568
#[tokio::test]
15161569
async fn test_status_403_is_bad_when_configured() {
15171570
// Start app server

‎src/main.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ async fn async_main() -> Result<(), Error> {
3030
// register the adapter as an extension
3131
adapter.register_default_extension();
3232
// check if the web application is ready
33-
adapter.check_init_health().await;
33+
adapter.check_init_health().await?;
3434
// start lambda runtime after the web application is ready
3535
adapter.run().await?;
3636

‎tests/integ_tests/main.rs‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,10 @@ async fn test_http_readiness_check() {
135135

136136
// Initialize adapter and do readiness check
137137
let mut adapter = Adapter::new(&options).expect("Failed to create adapter");
138-
adapter.check_init_health().await;
138+
adapter
139+
.check_init_health()
140+
.await
141+
.expect("init health check should succeed");
139142

140143
// Assert app server's healthcheck endpoint got called
141144
healthcheck.assert();
@@ -1109,7 +1112,10 @@ async fn test_http_async_init_ready_at_init() {
11091112
.expect("Failed to create adapter");
11101113

11111114
// Perform init health check — app is already running so it should succeed
1112-
adapter.check_init_health().await;
1115+
adapter
1116+
.check_init_health()
1117+
.await
1118+
.expect("init health check should succeed");
11131119

11141120
healthcheck.assert();
11151121

@@ -1142,7 +1148,10 @@ async fn test_http_tcp_readiness_check() {
11421148
.expect("Failed to create adapter");
11431149

11441150
// TCP readiness check should succeed since MockServer is listening
1145-
adapter.check_init_health().await;
1151+
adapter
1152+
.check_init_health()
1153+
.await
1154+
.expect("init health check should succeed");
11461155

11471156
// Now verify the adapter can still forward requests
11481157
let hello = app_server.mock(|when, then| {

0 commit comments

Comments
 (0)