[Rust Frontend][gRPC] Add engine-aware health reporting (#48992)

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Signed-off-by: Connor Carpenter <connorc@nvidia.com>

Connor Carpenter committed Jul 20, 2026 at 19:54 UTC 1940c8441eb39d7db88764b8e700d37e50573a84
9 files changed +333 -40
rust/Cargo.lock
+23 -8
@@ -4876,6 +4876,7 @@ dependencies = [
4876 "futures-core",
4877 "pin-project-lite",
4878 "tokio",
4879 + "tokio-util",
4880 ]
4881
4882 [[package]]
@@ -4937,9 +4938,9 @@ dependencies = [
4938
4939 [[package]]
4940 name = "tonic"
4940 -version = "0.14.5"
4941 +version = "0.14.6"
4942 source = "registry+https://github.com/rust-lang/crates.io-index"
4942 -checksum = "fec7c61a0695dc1887c1b53952990f3ad2e3a31453e1f49f10e75424943a93ec"
4943 +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef"
4944 dependencies = [
4945 "async-trait",
4946 "axum",
@@ -4966,9 +4967,9 @@ dependencies = [
4967
4968 [[package]]
4969 name = "tonic-build"
4969 -version = "0.14.5"
4970 +version = "0.14.6"
4971 source = "registry+https://github.com/rust-lang/crates.io-index"
4971 -checksum = "1882ac3bf5ef12877d7ed57aad87e75154c11931c2ba7e6cde5e22d63522c734"
4972 +checksum = "c68f61875ac5293cf72e6c8cf0158086428c82c37229e98c840878f1706b0322"
4973 dependencies = [
4974 "prettyplease",
4975 "proc-macro2",
@@ -4976,11 +4977,24 @@ dependencies = [
4977 "syn 2.0.117",
4978 ]
4979
4980 +[[package]]
4981 +name = "tonic-health"
4982 +version = "0.14.6"
4983 +source = "registry+https://github.com/rust-lang/crates.io-index"
4984 +checksum = "fcfab99db777fba2802f0dfa861d1628d1ae916fb199d29819941f139ae85082"
4985 +dependencies = [
4986 + "prost",
4987 + "tokio",
4988 + "tokio-stream",
4989 + "tonic",
4990 + "tonic-prost",
4991 +]
4992 +
4993 [[package]]
4994 name = "tonic-prost"
4981 -version = "0.14.5"
4995 +version = "0.14.6"
4996 source = "registry+https://github.com/rust-lang/crates.io-index"
4983 -checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309"
4997 +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0"
4998 dependencies = [
4999 "bytes",
5000 "prost",
@@ -4989,9 +5003,9 @@ dependencies = [
5003
5004 [[package]]
5005 name = "tonic-prost-build"
4992 -version = "0.14.5"
5006 +version = "0.14.6"
5007 source = "registry+https://github.com/rust-lang/crates.io-index"
4994 -checksum = "f3144df636917574672e93d0f56d7edec49f90305749c668df5101751bb8f95a"
5008 +checksum = "654e5643eff75d7f8c99197ce1440ed19a3474eada74c12bbac488b2cafdae27"
5009 dependencies = [
5010 "prettyplease",
5011 "proc-macro2",
@@ -5730,6 +5744,7 @@ dependencies = [
5744 "tokio-stream",
5745 "tokio-util",
5746 "tonic",
5747 + "tonic-health",
5748 "tonic-prost",
5749 "tonic-prost-build",
5750 "tower",
rust/Cargo.toml
+5 -4
@@ -118,10 +118,11 @@ tokio = { version = "1.47.1", features = [
118 tokio-openssl = "0.6"
119 tokio-stream = "0.1"
120 tokio-util = { version = "0.7.18", features = ["rt"] }
121 -tonic = "0.14.5"
122 -tonic-build = "0.14.5"
123 -tonic-prost = "0.14.5"
124 -tonic-prost-build = "0.14.5"
121 +tonic = "0.14.6"
122 +tonic-build = "0.14.6"
123 +tonic-health = "0.14.6"
124 +tonic-prost = "0.14.6"
125 +tonic-prost-build = "0.14.6"
126 tool-parser = "1.2.0"
127 tower = { version = "0.5.3", features = ["util"] }
128 tower-http = { version = "0.6.8", features = ["cors", "trace"] }
rust/src/engine-core-client/src/client.rs
+6
@@ -460,6 +460,12 @@ impl EngineCoreClient {
460 self.inner.is_healthy()
461 }
462
463 + /// Subscribe to engine health changes. The current value is `true` while
464 + /// the client is healthy and changes permanently to `false` on failure.
465 + pub fn subscribe_health(&self) -> tokio::sync::watch::Receiver<bool> {
466 + self.inner.subscribe_health()
467 + }
468 +
469 /// Return the first persistent health error observed by the client, if any.
470 pub fn health_error(&self) -> Option<Arc<Error>> {
471 self.inner.health_error()
rust/src/engine-core-client/src/client/imp.rs
+20 -1
@@ -9,7 +9,7 @@ use arc_swap::ArcSwapOption;
9 use parking_lot::Mutex;
10 use thiserror_ext::AsReport as _;
11 use tokio::runtime::Handle;
12 -use tokio::sync::mpsc;
12 +use tokio::sync::{mpsc, watch};
13 use tracing::{debug, info, trace, warn};
14 use vllm_metrics::METRICS;
15 use zeromq::RouterSendHalf;
@@ -36,6 +36,7 @@ pub(crate) struct ClientInner {
36 request_reg: Mutex<RequestRegistry>,
37 utility_reg: Mutex<UtilityRegistry>,
38 health_error: ArcSwapOption<Error>,
39 + health_tx: watch::Sender<bool>,
40 }
41
42 impl ClientInner {
@@ -57,6 +58,7 @@ impl ClientInner {
58 request_reg: Mutex::new(RequestRegistry::new(engines)),
59 utility_reg: Mutex::new(UtilityRegistry::default()),
60 health_error: ArcSwapOption::empty(),
61 + health_tx: watch::Sender::new(true),
62 }
63 }
64
@@ -169,6 +171,7 @@ impl ClientInner {
171 /// persistent health error.
172 pub fn close_registries(&self, error: Arc<Error>) {
173 let persistent_error = self.record_health_error(error);
174 + self.publish_unhealthy();
175 let request_senders = self.request_reg.lock().close();
176 let utility_senders = self.utility_reg.lock().close();
177
@@ -191,6 +194,12 @@ impl ClientInner {
194 self.health_error.load().is_none()
195 }
196
197 + /// Subscribe to engine health changes. The current value is `true` while
198 + /// the client is healthy and changes permanently to `false` on failure.
199 + pub fn subscribe_health(&self) -> watch::Receiver<bool> {
200 + self.health_tx.subscribe()
201 + }
202 +
203 /// Resolve one utility output to the waiting caller. Returns `true` if a
204 /// waiting caller existed.
205 pub fn resolve_utility_output(&self, output: UtilityOutput) -> bool {
@@ -280,6 +289,11 @@ impl ClientInner {
289 .expect("health error must be recorded before registries close")
290 }
291
292 + /// Publish the sticky healthy-to-unhealthy transition.
293 + fn publish_unhealthy(&self) {
294 + self.health_tx.send_if_modified(|healthy| std::mem::replace(healthy, false));
295 + }
296 +
297 /// Assert there is a recorded health error and return a `Shared` variant
298 /// wrapping it for error returns when the client is already closed.
299 fn closed_error(&self) -> Error {
@@ -461,13 +475,18 @@ mod tests {
475 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
476 async fn close_registries_records_first_health_error_only() {
477 let inner = test_inner().await;
478 + let mut health = inner.subscribe_health();
479 + assert!(*health.borrow());
480
481 inner.close_registries(Arc::new(Error::EngineCoreDead));
482 + health.changed().await.expect("health sender remains open");
483 assert!(!inner.is_healthy());
484 + assert!(!*health.borrow());
485 assert!(matches!(
486 inner.health_error().as_deref(),
487 Some(Error::EngineCoreDead)
488 ));
489 + assert!(!*inner.subscribe_health().borrow());
490
491 inner.close_registries(Arc::new(client_closed!("shutdown")));
492 assert!(matches!(
rust/src/server/Cargo.toml
+1
@@ -35,6 +35,7 @@ tokio-openssl.workspace = true
35 tokio-stream.workspace = true
36 tokio-util.workspace = true
37 tonic.workspace = true
38 +tonic-health.workspace = true
39 tonic-prost.workspace = true
40 tower.workspace = true
41 tower-http.workspace = true
rust/src/server/src/grpc/health.rs new
+70
@@ -0,0 +1,70 @@
1 +// SPDX-License-Identifier: Apache-2.0
2 +// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +
4 +use tokio::sync::watch;
5 +use tokio_util::sync::CancellationToken;
6 +use tonic::server::NamedService;
7 +use tonic_health::ServingStatus;
8 +use tonic_health::server::HealthReporter;
9 +use tracing::{info, warn};
10 +
11 +use super::GenerateGrpcService;
12 +
13 +pub(crate) async fn monitor_health(
14 + mut health_reporter: HealthReporter,
15 + mut engine_health: watch::Receiver<bool>,
16 + shutdown: CancellationToken,
17 +) {
18 + let generate_service = GenerateGrpcService::NAME;
19 + let status = ServingStatus::NotServing;
20 + let health_event_first = tokio::select! {
21 + result = engine_health.wait_for(|healthy| !*healthy) => {
22 + match result {
23 + Ok(_) => warn!(
24 + generate_service,
25 + overall_service = true,
26 + status = ?status,
27 + reason = "engine_unhealthy",
28 + "marking gRPC health services as not serving"
29 + ),
30 + Err(error) => warn!(
31 + %error,
32 + generate_service,
33 + overall_service = true,
34 + status = ?status,
35 + reason = "health_channel_closed",
36 + "engine health channel closed; marking gRPC health services as not serving"
37 + ),
38 + }
39 + true
40 + }
41 + _ = shutdown.cancelled() => {
42 + info!(
43 + generate_service,
44 + overall_service = true,
45 + status = ?status,
46 + reason = "server_shutdown",
47 + "server shutting down; marking gRPC health services as not serving"
48 + );
49 + false
50 + }
51 + };
52 +
53 + health_reporter.set_not_serving::<GenerateGrpcService>().await;
54 + // Generate is currently the only engine-backed gRPC service, so overall
55 + // server health intentionally mirrors it.
56 + health_reporter.set_service_status("", status).await;
57 +
58 + if health_event_first {
59 + shutdown.cancelled().await;
60 + info!(
61 + generate_service,
62 + overall_service = true,
63 + reason = "server_shutdown",
64 + "server shutting down; closing gRPC health watches"
65 + );
66 + }
67 +
68 + health_reporter.clear_service_status(generate_service).await;
69 + health_reporter.clear_service_status("").await;
70 +}
rust/src/server/src/grpc/mod.rs
+4
@@ -4,6 +4,7 @@
4 //! gRPC Generate service backed by the shared [`vllm_text::TextLlm`] facade.
5
6 mod convert;
7 +mod health;
8
9 use std::pin::Pin;
10 use std::sync::Arc;
@@ -24,8 +25,11 @@ pub mod pb {
25 tonic::include_proto!("vllm");
26 }
27
28 +pub(crate) use health::monitor_health;
29 pub use pb::generate_server::GenerateServer;
30
31 +pub(crate) type GenerateGrpcService = GenerateServer<GenerateServiceImpl>;
32 +
33 #[cfg(test)]
34 mod tests;
35
rust/src/server/src/grpc/tests.rs
+175 -12
@@ -16,6 +16,10 @@ use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
16 use tokio::net::TcpStream;
17 use tokio_openssl::SslStream;
18 use tonic::transport::{Channel, Endpoint, Server as TonicServer, Uri};
19 +use tonic_health::pb::HealthCheckRequest;
20 +use tonic_health::pb::health_check_response::ServingStatus as HealthServingStatus;
21 +use tonic_health::pb::health_client::HealthClient;
22 +use tonic_health::server::health_reporter;
23 use tower::service_fn;
24 use vllm_chat::{
25 ChatBackend, ChatLlm, ChatRenderer, ChatRequest, ChatTextBackend, DefaultChatOutputProcessor,
@@ -200,7 +204,11 @@ impl ChatRenderer for FakeTextBackend {
204 async fn setup_grpc_service(
205 engine_id: impl Into<EngineId>,
206 output_specs: Vec<(Vec<u32>, Option<EngineCoreFinishReason>)>,
203 -) -> (GenerateServer<GenerateServiceImpl>, MockEngineTask) {
207 +) -> (
208 + GenerateServer<GenerateServiceImpl>,
209 + tokio::sync::watch::Receiver<bool>,
210 + MockEngineTask,
211 +) {
212 let ipc = IpcNamespace::new().expect("create ipc namespace");
213 let handshake_address = ipc.handshake_endpoint();
214 let engine_id = engine_id.into();
@@ -232,6 +240,7 @@ async fn setup_grpc_service(
240 )
241 .await
242 .expect("connect client");
243 + let engine_health = client.subscribe_health();
244
245 let chat = ChatLlm::from_shared_backend(
246 test_llm(client),
@@ -240,6 +249,7 @@ async fn setup_grpc_service(
249 let state = Arc::new(AppState::new(vec!["test-model".to_string()], chat));
250 (
251 GenerateServer::new(GenerateServiceImpl::new(state)),
252 + engine_health,
253 engine_task,
254 )
255 }
@@ -254,25 +264,51 @@ async fn grpc_test_server(
264 tokio::task::JoinHandle<()>,
265 MockEngineTask,
266 ) {
257 - let (svc, engine_task) = setup_grpc_service(engine_id, output_specs).await;
267 + let (svc, engine_health, engine_task) = setup_grpc_service(engine_id, output_specs).await;
268 + let (channel, server_task) = start_grpc_test_server(
269 + svc,
270 + engine_health,
271 + tokio_util::sync::CancellationToken::new(),
272 + )
273 + .await;
274 + (GenerateClient::new(channel), server_task, engine_task)
275 +}
276 +
277 +async fn start_grpc_test_server(
278 + generate_service: GenerateServer<GenerateServiceImpl>,
279 + engine_health: tokio::sync::watch::Receiver<bool>,
280 + shutdown: tokio_util::sync::CancellationToken,
281 +) -> (Channel, tokio::task::JoinHandle<()>) {
282 + let (health_reporter, health_service) = health_reporter();
283 + health_reporter.set_serving::<GenerateServer<GenerateServiceImpl>>().await;
284
285 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind grpc listener");
286 let addr = listener.local_addr().expect("local addr");
287
288 let server_task = tokio::spawn(async move {
289 let incoming = MaybeTlsListener::plain(Listener::Tcp(listener));
264 - TonicServer::builder()
265 - .add_service(svc)
266 - .serve_with_incoming(incoming)
267 - .await
268 - .expect("grpc server");
290 + let server = TonicServer::builder()
291 + .add_service(health_service)
292 + .add_service(generate_service)
293 + .serve_with_incoming_shutdown(incoming, shutdown.clone().cancelled_owned());
294 + let health_monitor =
295 + super::monitor_health(health_reporter, engine_health, shutdown.clone());
296 + let server = async move {
297 + let result = server.await;
298 + shutdown.cancel();
299 + result
300 + };
301 + let (server_result, ()) = tokio::join!(server, health_monitor);
302 + server_result.expect("grpc server");
303 });
304
271 - let grpc_client = GenerateClient::connect(format!("http://{addr}"))
305 + let channel = Endpoint::from_shared(format!("http://{addr}"))
306 + .expect("grpc endpoint")
307 + .connect()
308 .await
273 - .expect("connect grpc client");
309 + .expect("connect grpc channel");
310
275 - (grpc_client, server_task, engine_task)
311 + (channel, server_task)
312 }
313
314 /// Spin up a TLS gRPC server (server cert from `certs`, `cert_reqs` mTLS mode).
@@ -283,7 +319,7 @@ async fn grpc_tls_test_server(
319 certs: &TestCerts,
320 cert_reqs: i32,
321 ) -> (String, tokio::task::JoinHandle<()>, MockEngineTask) {
286 - let (svc, engine_task) = setup_grpc_service(engine_id, output_specs).await;
322 + let (svc, _engine_health, engine_task) = setup_grpc_service(engine_id, output_specs).await;
323 let context = tls::build_grpc_server_config(&server_tls(certs, cert_reqs))
324 .expect("build grpc tls config");
325
@@ -373,7 +409,8 @@ async fn grpc_server_with_keepalive(
409 engine_id: impl Into<EngineId>,
410 keepalive: Option<Duration>,
411 ) -> (String, tokio::task::JoinHandle<()>, MockEngineTask) {
376 - let (svc, engine_task) = setup_grpc_service(engine_id, default_stream_output_specs()).await;
412 + let (svc, _engine_health, engine_task) =
413 + setup_grpc_service(engine_id, default_stream_output_specs()).await;
414
415 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind grpc listener");
416 let addr = listener.local_addr().expect("local addr").to_string();
@@ -1035,3 +1072,129 @@ async fn grpc_without_keepalive_keeps_unresponsive_connection_open() {
1072
1073 server_task.abort();
1074 }
1075 +
1076 +#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1077 +#[serial]
1078 +async fn grpc_health_transitions_to_not_serving_when_engine_becomes_unhealthy() {
1079 + let (generate_service, _connected_engine_health, _engine_task) =
1080 + setup_grpc_service(b"engine-grpc-health-failure", default_stream_output_specs()).await;
1081 + let (engine_health_tx, engine_health) = tokio::sync::watch::channel(true);
1082 + let (channel, server_task) = start_grpc_test_server(
1083 + generate_service,
1084 + engine_health,
1085 + tokio_util::sync::CancellationToken::new(),
1086 + )
1087 + .await;
1088 + let mut health_client = HealthClient::new(channel);
1089 +
1090 + let mut health_streams = Vec::new();
1091 + for service in ["vllm.Generate", ""] {
1092 + let service_label = if service.is_empty() {
1093 + "overall"
1094 + } else {
1095 + service
1096 + };
1097 + let mut stream = health_client
1098 + .watch(HealthCheckRequest {
1099 + service: service.to_string(),
1100 + })
1101 + .await
1102 + .unwrap_or_else(|error| {
1103 + panic!("failed to start health watch for {service_label}: {error}")
1104 + })
1105 + .into_inner();
1106 + let initial = stream
1107 + .message()
1108 + .await
1109 + .unwrap_or_else(|error| {
1110 + panic!("failed to read initial health status for {service_label}: {error}")
1111 + })
1112 + .unwrap_or_else(|| {
1113 + panic!("health watch for {service_label} ended before its initial status")
1114 + });
1115 + assert_eq!(
1116 + initial.status,
1117 + HealthServingStatus::Serving as i32,
1118 + "unexpected initial health status for {service_label}"
1119 + );
1120 + health_streams.push((service_label, stream));
1121 + }
1122 +
1123 + engine_health_tx.send(false).expect("publish unhealthy engine state");
1124 +
1125 + for (service_label, mut stream) in health_streams {
1126 + let update = tokio::time::timeout(Duration::from_secs(2), stream.message())
1127 + .await
1128 + .unwrap_or_else(|_| panic!("timed out waiting for health update for {service_label}"))
1129 + .unwrap_or_else(|error| {
1130 + panic!("failed to read health update for {service_label}: {error}")
1131 + })
1132 + .unwrap_or_else(|| panic!("health watch for {service_label} ended before its update"));
1133 + assert_eq!(
1134 + update.status,
1135 + HealthServingStatus::NotServing as i32,
1136 + "unexpected health status for {service_label}"
1137 + );
1138 + }
1139 +
1140 + server_task.abort();
1141 +}
1142 +
1143 +#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1144 +#[serial]
1145 +async fn grpc_health_watch_closes_on_graceful_shutdown() {
1146 + let (generate_service, engine_health, _engine_task) = setup_grpc_service(
1147 + b"engine-grpc-health-shutdown",
1148 + default_stream_output_specs(),
1149 + )
1150 + .await;
1151 + let shutdown = tokio_util::sync::CancellationToken::new();
1152 + let (channel, server_task) =
1153 + start_grpc_test_server(generate_service, engine_health, shutdown.clone()).await;
1154 + let mut health_client = HealthClient::new(channel);
1155 + let mut stream = health_client
1156 + .watch(HealthCheckRequest {
1157 + service: "vllm.Generate".to_string(),
1158 + })
1159 + .await
1160 + .expect("start health watch for vllm.Generate")
1161 + .into_inner();
1162 +
1163 + let initial = stream
1164 + .message()
1165 + .await
1166 + .expect("read initial health status for vllm.Generate")
1167 + .expect("health watch ended before its initial status");
1168 + assert_eq!(
1169 + initial.status,
1170 + HealthServingStatus::Serving as i32,
1171 + "unexpected initial health status for vllm.Generate"
1172 + );
1173 +
1174 + shutdown.cancel();
1175 +
1176 + let update = tokio::time::timeout(Duration::from_secs(2), stream.message())
1177 + .await
1178 + .expect("timed out waiting for shutdown health update for vllm.Generate")
1179 + .expect("failed to read shutdown health update for vllm.Generate")
1180 + .expect("health watch ended before its shutdown update");
1181 + assert_eq!(
1182 + update.status,
1183 + HealthServingStatus::NotServing as i32,
1184 + "unexpected shutdown health status for vllm.Generate"
1185 + );
1186 +
1187 + let stream_end = tokio::time::timeout(Duration::from_secs(2), stream.message())
1188 + .await
1189 + .expect("timed out waiting for vllm.Generate health watch to close")
1190 + .expect("failed while closing vllm.Generate health watch");
1191 + assert!(
1192 + stream_end.is_none(),
1193 + "vllm.Generate health watch remained open"
1194 + );
1195 +
1196 + tokio::time::timeout(Duration::from_secs(2), server_task)
1197 + .await
1198 + .expect("timed out waiting for gRPC server shutdown")
1199 + .expect("gRPC server task failed");
1200 +}
rust/src/server/src/lib.rs
+29 -15
@@ -39,6 +39,7 @@ use tokio::net::TcpListener;
39 use tokio::time::{Instant, sleep_until};
40 use tokio_util::sync::CancellationToken;
41 use tonic::transport::Server as TonicServer;
42 +use tonic_health::server::health_reporter;
43 use tower::ServiceExt as _;
44 use tracing::{info, trace, warn};
45 use vllm_chat::{ChatLlm, LoadModelBackendsOptions, load_model_backends};
@@ -203,14 +204,19 @@ where
204 .map(tls::build_grpc_server_config)
205 .transpose()
206 .context("invalid gRPC TLS configuration")?;
206 - let svc = grpc::GenerateServer::new(grpc::GenerateServiceImpl::new(state.clone()));
207 + let (health_reporter, health_service) = health_reporter();
208 + let engine_health = state.engine_core_client().subscribe_health();
209 + health_reporter.set_serving::<grpc::GenerateGrpcService>().await;
210 + let generate_service =
211 + grpc::GenerateGrpcService::new(grpc::GenerateServiceImpl::new(state.clone()));
212 let svc = TonicServer::builder()
213 .http2_keepalive_interval(Some(GRPC_KEEPALIVE_INTERVAL))
214 .http2_keepalive_timeout(Some(GRPC_KEEPALIVE_TIMEOUT))
215 .layer(middleware::request_runtime_layer(state.clone()))
211 - .add_service(svc);
216 + .add_service(health_service)
217 + .add_service(generate_service);
218 info!(%addr, tls = grpc_tls.is_some(), "starting gRPC server");
213 - Some((grpc_listener, svc, grpc_tls))
219 + Some((grpc_listener, svc, grpc_tls, health_reporter, engine_health))
220 } else {
221 None
222 };
@@ -294,7 +300,8 @@ where
300 let server_shutdown = server_shutdown.clone();
301 let force_shutdown = force_shutdown.clone();
302 async move {
297 - let Some((grpc_listener, svc, grpc_tls)) = grpc_setup else {
303 + let Some((grpc_listener, svc, grpc_tls, health_reporter, engine_health)) = grpc_setup
304 + else {
305 // No gRPC configured: just wait for shutdown so we do not race the
306 // join! by resolving early and tripping the cancellation token.
307 shutdown.cancelled().await;
@@ -304,19 +311,26 @@ where
311 Some(context) => MaybeTlsListener::tls(grpc_listener, context),
312 None => MaybeTlsListener::plain(grpc_listener),
313 };
307 - let server = svc.serve_with_incoming_shutdown(incoming, shutdown.cancelled_owned());
308 -
309 - let result = tokio::select! {
310 - result = server => {
311 - result.context("gRPC server failed")
312 - }
313 - _ = force_shutdown.cancelled() => {
314 - warn!("gRPC graceful shutdown deadline elapsed; aborting server");
315 - Ok(())
316 - }
314 + let server =
315 + svc.serve_with_incoming_shutdown(incoming, shutdown.clone().cancelled_owned());
316 + let health_monitor = grpc::monitor_health(health_reporter, engine_health, shutdown);
317 +
318 + let server = async move {
319 + let result = tokio::select! {
320 + result = server => {
321 + result.context("gRPC server failed")
322 + }
323 + _ = force_shutdown.cancelled() => {
324 + warn!("gRPC graceful shutdown deadline elapsed; aborting server");
325 + Ok(())
326 + }
327 + };
328 +
329 + server_shutdown.cancel();
330 + result
331 };
332
319 - server_shutdown.cancel();
333 + let (result, ()) = tokio::join!(server, health_monitor);
334 result
335 }
336 };