diff --git a/README.md b/README.md index ac89b9e..4f0481d 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,8 @@ docker run --rm --network host -v impossible-inferences-data:/data impossible-in ``` The first `docker run` is setup; the second is serve. Ordinary container startup is offline after -setup. Bridge-port publishing is intentionally unsupported because the server refuses non-loopback +setup. `docker stop` (SIGTERM) and Ctrl+C both drain in-flight requests and stop the private runtime +within the configured shutdown timeout. Bridge-port publishing is intentionally unsupported because the server refuses non-loopback binds. ## License diff --git a/crates/impossible-inferences-server/src/main.rs b/crates/impossible-inferences-server/src/main.rs index 17c82a5..078b0de 100644 --- a/crates/impossible-inferences-server/src/main.rs +++ b/crates/impossible-inferences-server/src/main.rs @@ -251,7 +251,7 @@ async fn serve(config: EffectiveConfig) -> Result<(), Box let cancellation = CancellationToken::new(); let signal = cancellation.clone(); tokio::spawn(async move { - if tokio::signal::ctrl_c().await.is_ok() { + if shutdown_requested().await { let _ = signal.cancel(); } }); @@ -291,6 +291,22 @@ async fn serve(config: EffectiveConfig) -> Result<(), Box Ok(()) } +/// Waits for Ctrl+C or, on Unix, SIGTERM (`docker stop`, `kill`, service managers). +/// +/// Returns `false` only when no shutdown signal can be observed. +async fn shutdown_requested() -> bool { + #[cfg(unix)] + if let Ok(mut terminate) = + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + { + return tokio::select! { + result = tokio::signal::ctrl_c() => result.is_ok(), + received = terminate.recv() => received.is_some(), + }; + } + tokio::signal::ctrl_c().await.is_ok() +} + const fn unavailable_engine_status() -> EngineStatus { EngineStatus { ready: false, diff --git a/crates/impossible-inferences-server/tests/binary.rs b/crates/impossible-inferences-server/tests/binary.rs index 8f1510f..d7b6642 100644 --- a/crates/impossible-inferences-server/tests/binary.rs +++ b/crates/impossible-inferences-server/tests/binary.rs @@ -109,3 +109,45 @@ fn real_binary_enforces_public_failures_and_body_deadline() -> Result<(), Box Result<(), Box> { + let reservation = TcpListener::bind(("127.0.0.1", 0))?; + let port = reservation.local_addr()?.port(); + drop(reservation); + let child = Command::new(env!("CARGO_BIN_EXE_impossible-inferences-server")) + .args([ + "serve", + "--bind", + &format!("127.0.0.1:{port}"), + "--shutdown-timeout-ms", + "1000", + ]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()?; + let mut child = ChildGuard(child); + wait_until_live(port)?; + + let status = Command::new("kill") + .args(["-TERM", &child.0.id().to_string()]) + .status()?; + assert!(status.success(), "kill -TERM failed: {status}"); + + let deadline = Instant::now() + Duration::from_secs(5); + let exit = loop { + if let Some(exit) = child.0.try_wait()? { + break exit; + } + if Instant::now() >= deadline { + return Err("server did not exit after SIGTERM".into()); + } + thread::sleep(Duration::from_millis(25)); + }; + assert!( + exit.success(), + "SIGTERM did not trigger graceful shutdown: {exit}" + ); + Ok(()) +}