@@ -8,6 +8,7 @@ mod metrics;
88mod observability;
99mod response;
1010mod routing_log;
11+ mod shutdown;
1112mod sse;
1213mod stats;
1314mod usage_metrics;
@@ -50,6 +51,9 @@ pub use observability::{flush_observability, initialize_observability};
5051/// Default TCP listen backlog used by the Rust server.
5152pub const DEFAULT_LISTEN_BACKLOG : u32 = 65_535 ;
5253
54+ /// Default time allowed for active requests to finish during shutdown.
55+ pub const DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT : Duration = Duration :: from_secs ( 30 ) ;
56+
5357/// Maximum buffered JSON request size accepted by the LLM endpoints.
5458pub const DEFAULT_MAX_REQUEST_BODY_BYTES : usize = 32 * 1024 * 1024 ;
5559
@@ -214,6 +218,8 @@ pub struct ServerRunOptions {
214218 pub backlog : u32 ,
215219 /// Validate runtime construction without binding a socket.
216220 pub dry_run : bool ,
221+ /// Maximum time active requests may drain after shutdown begins.
222+ pub shutdown_timeout : Duration ,
217223 /// TLS certificate configuration, when HTTPS is enabled.
218224 pub tls : Option < TlsOptions > ,
219225}
@@ -242,7 +248,7 @@ pub async fn run_server(state: ServerState, options: ServerRunOptions) -> Server
242248
243249 let server = BoundServer :: bind ( state, options) ?;
244250 println ! ( "{}" , server. startup_banner( std:: io:: stdout( ) . is_terminal( ) ) ) ;
245- server. serve ( shutdown_signal ( ) ) . await
251+ server. serve ( shutdown :: signal ( ) ) . await
246252}
247253
248254/// A configured server with its listening socket already bound.
@@ -276,10 +282,11 @@ impl BoundServer {
276282 self ,
277283 shutdown : impl Future < Output = ( ) > + Send + ' static ,
278284 ) -> ServerResult < ( ) > {
285+ let shutdown_timeout = self . options . shutdown_timeout ;
279286 if let Some ( tls) = self . options . tls {
280- serve_tls ( self . listener , self . router , tls, shutdown) . await
287+ serve_tls ( self . listener , self . router , tls, shutdown_timeout , shutdown) . await
281288 } else {
282- serve ( self . listener , self . router , shutdown) . await
289+ serve ( self . listener , self . router , shutdown_timeout , shutdown) . await
283290 }
284291 }
285292
@@ -292,6 +299,7 @@ async fn serve_tls(
292299 listener : TcpListener ,
293300 router : Router ,
294301 tls : TlsOptions ,
302+ shutdown_timeout : Duration ,
295303 shutdown : impl Future < Output = ( ) > + Send + ' static ,
296304) -> ServerResult < ( ) > {
297305 if let Err ( error) = rustls:: crypto:: aws_lc_rs:: default_provider ( ) . install_default ( ) {
@@ -301,32 +309,49 @@ async fn serve_tls(
301309 let config = RustlsConfig :: from_pem_file ( tls. cert , tls. key )
302310 . await
303311 . map_err ( server_io_error) ?;
304- let handle = axum_server:: Handle :: new ( ) ;
305-
306- let shutdown_handle = handle. clone ( ) ;
307- tokio:: spawn ( async move {
308- shutdown. await ;
309- shutdown_handle. graceful_shutdown ( Some ( Duration :: from_secs ( 2 ) ) ) ;
310- } ) ;
311-
312312 let std_listener = listener. into_std ( ) . map_err ( server_io_error) ?;
313- axum_server:: from_tcp_rustls ( std_listener, config)
314- . map_err ( server_io_error ) ?
315- . handle ( handle )
316- . serve ( router . into_make_service ( ) )
317- . await
318- . map_err ( server_io_error )
313+ let server = axum_server:: from_tcp_rustls ( std_listener, config) . map_err ( server_io_error ) ? ;
314+ let handle = axum_server :: Handle :: new ( ) ;
315+ let server = server
316+ . handle ( handle . clone ( ) )
317+ . serve ( router . into_make_service ( ) ) ;
318+ serve_until_shutdown ( server , handle , shutdown_timeout , shutdown ) . await
319319}
320320
321321async fn serve (
322322 listener : TcpListener ,
323323 router : Router ,
324+ shutdown_timeout : Duration ,
324325 shutdown : impl Future < Output = ( ) > + Send + ' static ,
325326) -> ServerResult < ( ) > {
326- axum:: serve ( listener, router)
327- . with_graceful_shutdown ( shutdown)
328- . await
329- . map_err ( server_io_error)
327+ let std_listener = listener. into_std ( ) . map_err ( server_io_error) ?;
328+ let server = axum_server:: from_tcp ( std_listener) . map_err ( server_io_error) ?;
329+ let handle = axum_server:: Handle :: new ( ) ;
330+ let server = server
331+ . handle ( handle. clone ( ) )
332+ . serve ( router. into_make_service ( ) ) ;
333+ serve_until_shutdown ( server, handle, shutdown_timeout, shutdown) . await
334+ }
335+
336+ /// Runs the server until it exits or shutdown begins, then drains active requests.
337+ async fn serve_until_shutdown (
338+ server : impl Future < Output = std:: io:: Result < ( ) > > ,
339+ handle : axum_server:: Handle < SocketAddr > ,
340+ timeout : Duration ,
341+ shutdown : impl Future < Output = ( ) > + Send + ' static ,
342+ ) -> ServerResult < ( ) > {
343+ tokio:: pin!( server) ;
344+ tokio:: select! {
345+ result = & mut server => result. map_err( server_io_error) ,
346+ _ = shutdown => {
347+ tracing:: info!(
348+ ?timeout,
349+ "shutdown signal received; draining active requests"
350+ ) ;
351+ handle. graceful_shutdown( Some ( timeout) ) ;
352+ server. await . map_err( server_io_error)
353+ }
354+ }
330355}
331356
332357/// Ingress timestamp for one request, taken before any body is read.
@@ -411,16 +436,6 @@ fn server_io_error(error: std::io::Error) -> ServerError {
411436 ServerError :: new ( error. to_string ( ) )
412437}
413438
414- async fn shutdown_signal ( ) {
415- if let Err ( error) = tokio:: signal:: ctrl_c ( ) . await {
416- tracing:: warn!(
417- error = %error,
418- "ctrl-c shutdown signal unavailable; continuing without shutdown trigger"
419- ) ;
420- std:: future:: pending :: < ( ) > ( ) . await ;
421- }
422- }
423-
424439async fn openai_chat_completions (
425440 State ( state) : State < ServerState > ,
426441 Extension ( started) : Extension < RequestStart > ,
@@ -1079,8 +1094,111 @@ fn endpoint_listing(has_routing_log: bool) -> String {
10791094
10801095#[ cfg( test) ]
10811096mod tests {
1097+ use tokio:: io:: { AsyncReadExt , AsyncWriteExt } ;
1098+ use tokio:: sync:: { Notify , oneshot} ;
1099+
10821100 use super :: * ;
10831101
1102+ #[ derive( Clone ) ]
1103+ struct ShutdownTestState {
1104+ started : Arc < Notify > ,
1105+ release : Arc < Notify > ,
1106+ }
1107+
1108+ struct ShutdownTestServer {
1109+ state : ShutdownTestState ,
1110+ shutdown : oneshot:: Sender < ( ) > ,
1111+ server : task:: JoinHandle < ServerResult < ( ) > > ,
1112+ request : task:: JoinHandle < std:: io:: Result < Vec < u8 > > > ,
1113+ }
1114+
1115+ async fn blocked_request ( State ( state) : State < ShutdownTestState > ) -> & ' static str {
1116+ state. started . notify_one ( ) ;
1117+ state. release . notified ( ) . await ;
1118+ "done"
1119+ }
1120+
1121+ async fn raw_request ( addr : SocketAddr ) -> std:: io:: Result < Vec < u8 > > {
1122+ let mut stream = tokio:: net:: TcpStream :: connect ( addr) . await ?;
1123+ stream
1124+ . write_all ( b"GET / HTTP/1.1\r \n Host: localhost\r \n Connection: close\r \n \r \n " )
1125+ . await ?;
1126+ let mut response = Vec :: new ( ) ;
1127+ stream. read_to_end ( & mut response) . await ?;
1128+ Ok ( response)
1129+ }
1130+
1131+ fn shutdown_test_server ( shutdown_timeout : Duration ) -> ShutdownTestServer {
1132+ let state = ShutdownTestState {
1133+ started : Arc :: new ( Notify :: new ( ) ) ,
1134+ release : Arc :: new ( Notify :: new ( ) ) ,
1135+ } ;
1136+ let router = Router :: new ( )
1137+ . route ( "/" , get ( blocked_request) )
1138+ . with_state ( state. clone ( ) ) ;
1139+ let listener = bind_tcp_listener ( "127.0.0.1:0" . parse ( ) . expect ( "valid address" ) , 16 )
1140+ . expect ( "listener binds" ) ;
1141+ let addr = listener. local_addr ( ) . expect ( "listener has an address" ) ;
1142+ let ( shutdown, shutdown_receiver) = oneshot:: channel ( ) ;
1143+ let server = tokio:: spawn ( serve ( listener, router, shutdown_timeout, async move {
1144+ let _ = shutdown_receiver. await ;
1145+ } ) ) ;
1146+ let request = tokio:: spawn ( raw_request ( addr) ) ;
1147+ ShutdownTestServer {
1148+ state,
1149+ shutdown,
1150+ server,
1151+ request,
1152+ }
1153+ }
1154+
1155+ // Active requests may finish within the grace period, while stuck requests are bounded.
1156+ #[ tokio:: test]
1157+ async fn shutdown_drains_until_configured_deadline ( ) {
1158+ let ShutdownTestServer {
1159+ state,
1160+ shutdown,
1161+ mut server,
1162+ request,
1163+ } = shutdown_test_server ( Duration :: from_secs ( 1 ) ) ;
1164+ state. started . notified ( ) . await ;
1165+ shutdown. send ( ( ) ) . expect ( "server receives shutdown" ) ;
1166+ assert ! (
1167+ tokio:: time:: timeout( Duration :: from_millis( 25 ) , & mut server)
1168+ . await
1169+ . is_err( ) ,
1170+ "server must wait for the active request"
1171+ ) ;
1172+ state. release . notify_one ( ) ;
1173+ tokio:: time:: timeout ( Duration :: from_secs ( 1 ) , server)
1174+ . await
1175+ . expect ( "server stops after request drains" )
1176+ . expect ( "server task completes" )
1177+ . expect ( "server exits cleanly" ) ;
1178+ let response = request
1179+ . await
1180+ . expect ( "request task completes" )
1181+ . expect ( "request succeeds" ) ;
1182+ assert ! ( response. windows( 8 ) . any( |part| part == b"200 OK\r \n " ) ) ;
1183+ assert ! ( response. ends_with( b"done" ) ) ;
1184+
1185+ let ShutdownTestServer {
1186+ state,
1187+ shutdown,
1188+ server,
1189+ request,
1190+ } = shutdown_test_server ( Duration :: from_millis ( 25 ) ) ;
1191+ state. started . notified ( ) . await ;
1192+ shutdown. send ( ( ) ) . expect ( "server receives shutdown" ) ;
1193+ tokio:: time:: timeout ( Duration :: from_secs ( 1 ) , server)
1194+ . await
1195+ . expect ( "shutdown deadline is enforced" )
1196+ . expect ( "server task completes" )
1197+ . expect ( "server exits cleanly" ) ;
1198+ state. release . notify_one ( ) ;
1199+ request. abort ( ) ;
1200+ }
1201+
10841202 // Terminal request severity follows HTTP status instead of error-path bookkeeping.
10851203 #[ test]
10861204 fn request_log_level_follows_http_status ( ) {
0 commit comments