diff --git a/tonic/src/transport/server/mod.rs b/tonic/src/transport/server/mod.rs index af89c2f9d..8cb4e8c7c 100644 --- a/tonic/src/transport/server/mod.rs +++ b/tonic/src/transport/server/mod.rs @@ -930,10 +930,12 @@ fn serve_connection( let mut conn = pin!(builder.serve_connection(hyper_io, hyper_svc)); - let mut connection_timeout = pin!(connection_timeout_future( - max_connection_age, - max_connection_age_grace, - )); + let mut connection_timeout = pin!(Fuse { + inner: Some(connection_timeout_future( + max_connection_age, + max_connection_age_grace, + )) + }); loop { tokio::select! { @@ -1328,6 +1330,36 @@ mod tests { assert!(matches!(action, TimeoutAction::ForcefulShutdown)); } + #[tokio::test(start_paused = true)] + async fn test_connection_timeout_fused_after_graceful_shutdown() { + // Reproduce #2522: connection_timeout polled after GracefulShutdown + let mut future = pin!(Fuse { + inner: Some(connection_timeout_future( + Some(Duration::from_secs(10)), + None, + )) + }); + + // First poll: should return GracefulShutdown after 10s + let action = tokio::select! { + action = &mut future => action, + _ = tokio::time::sleep(Duration::from_secs(11)) => { + panic!("timeout future should complete after max_connection_age"); + } + }; + assert!(matches!(action, TimeoutAction::GracefulShutdown)); + + // Second poll: Fuse should return Pending, not panic + tokio::select! { + _ = &mut future => { + panic!("fused future should not complete again"); + } + _ = tokio::time::sleep(Duration::from_secs(1)) => { + // OK: future is fused, returns Pending + } + } + } + #[test] fn server_tcp_defaults() { const EXAMPLE_TCP_KEEPALIVE: Duration = Duration::from_secs(10);