1use std::future::Future;
5use std::net::SocketAddr;
6use std::pin::Pin;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::{Arc, Mutex, OnceLock, PoisonError};
9use std::task::{Context, Poll};
10use std::time::Duration;
11
12use autumn_web::AppState;
13use autumn_web::actuator::MetricFamily;
14use futures_util::{Stream, StreamExt as _};
15use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
16use tokio::net::TcpStream;
17use tokio::sync::{OwnedSemaphorePermit, Semaphore, watch};
18use tokio::task::JoinHandle;
19use tokio_util::sync::{CancellationToken, PollSemaphore, WaitForCancellationFutureOwned};
20use tokio_util::task::TaskTracker;
21use tonic::service::Routes;
22use tonic::transport::server::{Connected, TcpConnectInfo, TcpIncoming};
23use tonic_health::ServingStatus;
24use tonic_health::server::HealthReporter;
25
26use crate::config::{GrpcConfig, Listener};
27use crate::error::GrpcError;
28use crate::lifecycle::{Lifecycle, LifecycleCell, LifecycleEvent};
29use crate::metrics::Metrics;
30
31pub struct Shared {
33 pub lifecycle: LifecycleCell,
34 pub local_addr: OnceLock<SocketAddr>,
35 pub health: OnceLock<HealthReporter>,
36 pub health_names: OnceLock<Vec<String>>,
38 pub metrics: Arc<Metrics>,
39 pub listener: OnceLock<Listener>,
41 pub gate: OnceLock<crate::gate::Target>,
43 pub calls: TaskTracker,
45 grace_ms: AtomicU64,
46 stop: CancellationToken,
48 pub kill: CancellationToken,
51 task: Mutex<Option<JoinHandle<()>>>,
52 stopped: watch::Sender<bool>,
53}
54
55impl Shared {
56 pub fn new() -> Self {
57 Self {
58 lifecycle: LifecycleCell::new(),
59 local_addr: OnceLock::new(),
60 health: OnceLock::new(),
61 health_names: OnceLock::new(),
62 metrics: Arc::new(Metrics::new()),
63 listener: OnceLock::new(),
64 gate: OnceLock::new(),
65 calls: TaskTracker::new(),
66 grace_ms: AtomicU64::new(GrpcConfig::default().shutdown_grace_ms),
67 stop: CancellationToken::new(),
68 kill: CancellationToken::new(),
69 task: Mutex::new(None),
70 stopped: watch::channel(false).0,
71 }
72 }
73
74 fn grace(&self) -> Duration {
75 Duration::from_millis(self.grace_ms.load(Ordering::Acquire))
76 }
77
78 pub fn mark_stopped(&self) {
79 self.stopped.send_replace(true);
80 }
81
82 async fn set_health(&self, status: ServingStatus) {
84 if let Some(reporter) = self.health.get() {
85 reporter.set_service_status("", status).await;
86 for name in self.health_names.get().into_iter().flatten() {
87 reporter.set_service_status(name, status).await;
88 }
89 }
90 }
91
92 fn request_shutdown(self: &Arc<Self>) {
94 match self.lifecycle.apply(LifecycleEvent::ShutdownRequested) {
95 Ok(Lifecycle::Draining) => {
96 tokio::spawn(self.clone().drain());
97 }
98 Ok(_) => self.mark_stopped(),
100 Err(_) => {}
101 }
102 }
103
104 async fn drain(self: Arc<Self>) {
107 self.set_health(ServingStatus::NotServing).await;
108 if let Some(reporter) = self.health.get() {
111 let mut reporter = reporter.clone();
112 reporter.clear_service_status("").await;
113 for name in self.health_names.get().into_iter().flatten() {
114 reporter.clear_service_status(name).await;
115 }
116 }
117 self.stop.cancel();
118 let task = self
119 .task
120 .lock()
121 .unwrap_or_else(PoisonError::into_inner)
122 .take();
123 let grace = self.grace();
124 if let Some(mut task) = task
125 && tokio::time::timeout(grace, &mut task).await.is_err()
126 {
127 tracing::warn!(
128 grace_ms = grace.as_millis(),
129 "gRPC calls still open after the grace period; closing connections"
130 );
131 self.kill.cancel();
132 if tokio::time::timeout(KILL_WAIT, &mut task).await.is_err() {
133 task.abort();
134 }
135 }
136 self.calls.close();
139 if tokio::time::timeout(grace, self.calls.wait())
140 .await
141 .is_err()
142 {
143 tracing::warn!(
144 grace_ms = grace.as_millis(),
145 "gRPC calls still open after the grace period; ending them"
146 );
147 self.kill.cancel();
148 let _ = tokio::time::timeout(KILL_WAIT, self.calls.wait()).await;
149 }
150 let _ = self.lifecycle.apply(LifecycleEvent::Drained);
151 self.mark_stopped();
152 tracing::info!("gRPC server stopped");
153 }
154}
155
156pub const KILL_WAIT: Duration = Duration::from_secs(1);
158
159#[derive(Clone)]
171pub struct GrpcHandle {
172 shared: Arc<Shared>,
173}
174
175impl std::fmt::Debug for GrpcHandle {
176 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177 f.debug_struct("GrpcHandle")
178 .field("state", &self.state())
179 .field("local_addr", &self.local_addr())
180 .finish_non_exhaustive()
181 }
182}
183
184impl GrpcHandle {
185 #[allow(clippy::redundant_pub_crate)] pub(crate) const fn new(shared: Arc<Shared>) -> Self {
187 Self { shared }
188 }
189
190 #[must_use]
193 pub fn local_addr(&self) -> Option<SocketAddr> {
194 self.shared.local_addr.get().copied()
195 }
196
197 #[must_use]
199 pub fn state(&self) -> Lifecycle {
200 self.shared.lifecycle.get()
201 }
202
203 #[must_use]
206 pub fn health_reporter(&self) -> Option<HealthReporter> {
207 self.shared.health.get().cloned()
208 }
209
210 #[must_use]
213 pub fn shutdown_grace(&self) -> Duration {
214 self.shared.grace()
215 }
216
217 #[must_use]
219 pub fn metric_families(&self) -> Vec<MetricFamily> {
220 self.shared.metrics.families(self.state().is_ready())
221 }
222
223 pub async fn shutdown(&self) {
240 let shared = &self.shared;
241 shared.request_shutdown();
242 let mut stopped = shared.stopped.subscribe();
243 let _ = stopped.wait_for(|done| *done).await;
244 }
245}
246
247pub struct Launch {
249 pub config: GrpcConfig,
250 pub development: bool,
251 pub routes: Routes,
252 pub health: Option<HealthReporter>,
253 pub health_names: Vec<String>,
254 #[cfg(feature = "tls")]
255 pub tls: Option<tonic::transport::ServerTlsConfig>,
256}
257
258pub async fn start(shared: &Arc<Shared>, launch: Launch) -> Result<SocketAddr, GrpcError> {
260 let Launch {
261 config,
262 development,
263 routes,
264 health,
265 health_names,
266 #[cfg(feature = "tls")]
267 tls,
268 } = launch;
269 if shared.lifecycle.get() != Lifecycle::Idle {
270 return Err(GrpcError::NotIdle(shared.lifecycle.get()));
271 }
272 let addr = config.bind_addr(development)?;
273 let listener = bind(addr).map_err(|source| GrpcError::Bind { addr, source })?;
274 let local_addr = listener
275 .local_addr()
276 .map_err(|source| GrpcError::Bind { addr, source })?;
277
278 let mut builder = tonic::transport::Server::builder()
281 .max_concurrent_streams(Some(config.max_concurrent_streams))
282 .http2_max_local_error_reset_streams(Some(config.http2_max_local_error_reset_streams))
283 .http2_keepalive_interval(config.http2_keepalive_interval())
284 .trace_fn(|request| tracing::debug_span!("grpc", path = %request.uri().path()));
285 if config.concurrency_limit_per_connection > 0 {
286 builder = builder.concurrency_limit_per_connection(config.concurrency_limit_per_connection);
287 }
288 if let Some(timeout) = config.timeout() {
289 builder = builder.timeout(timeout);
290 }
291 if let Some(timeout) = config.http2_keepalive_timeout() {
292 builder = builder.http2_keepalive_timeout(Some(timeout));
293 }
294 if let Some(age) = config.max_connection_age() {
295 builder = builder.max_connection_age(age);
296 }
297 #[cfg(feature = "tls")]
298 if let Some(tls) = tls {
299 builder = builder
300 .tls_config(tls)
301 .map_err(|e| GrpcError::Tls(e.to_string()))?;
302 }
303
304 if let Some(reporter) = health {
305 let _ = shared.health.set(reporter);
306 }
307 let _ = shared.health_names.set(health_names);
308 shared.set_health(ServingStatus::Serving).await;
309 shared
310 .grace_ms
311 .store(config.shutdown_grace_ms, Ordering::Release);
312 let _ = shared.local_addr.set(local_addr);
313 let _ = shared.listener.set(Listener::Dedicated);
314
315 let stop = shared.stop.clone();
316 let exit = ExitGuard(shared.clone());
317 let server = builder.add_routes(routes).serve_with_incoming_shutdown(
318 Accept::new(listener, &config, shared),
319 stop.cancelled_owned(),
320 );
321 let task = tokio::spawn(async move {
322 let _exit = exit;
323 if let Err(error) = server.await {
324 tracing::error!(%error, "gRPC server ended with an error");
325 }
326 });
327 *shared.task.lock().unwrap_or_else(PoisonError::into_inner) = Some(task);
328 if let Err(state) = shared.lifecycle.apply(LifecycleEvent::Bound) {
329 shared.stop.cancel();
331 return Err(GrpcError::NotIdle(state));
332 }
333 Ok(local_addr)
334}
335
336pub async fn start_shared(shared: &Arc<Shared>, launch: Launch) -> Result<(), GrpcError> {
339 let Launch {
340 config,
341 routes,
342 health,
343 health_names,
344 ..
345 } = launch;
346 if shared.lifecycle.get() != Lifecycle::Idle {
347 return Err(GrpcError::NotIdle(shared.lifecycle.get()));
348 }
349 if let Some(reporter) = health {
350 let _ = shared.health.set(reporter);
351 }
352 let _ = shared.health_names.set(health_names);
353 shared.set_health(ServingStatus::Serving).await;
354 shared
355 .grace_ms
356 .store(config.shutdown_grace_ms, Ordering::Release);
357 let _ = shared.listener.set(Listener::Shared);
358 let _ = shared.gate.set(crate::gate::Target {
359 router: routes.into_axum_router(),
360 timeout: config.timeout(),
361 });
362 shared
363 .lifecycle
364 .apply(LifecycleEvent::Bound)
365 .map(|_| ())
366 .map_err(GrpcError::NotIdle)
367}
368
369struct ExitGuard(Arc<Shared>);
371
372impl Drop for ExitGuard {
373 fn drop(&mut self) {
374 if self.0.lifecycle.apply(LifecycleEvent::ServerExited) == Ok(Lifecycle::Failed) {
375 tracing::error!("gRPC server stopped unexpectedly");
376 self.0.mark_stopped();
377 }
378 }
379}
380
381pub fn watch_readiness(shared: &Arc<Shared>, state: AppState) {
386 if shared.health.get().is_none() {
387 return;
388 }
389 let shared = shared.clone();
390 let stop = shared.stop.clone();
391 tokio::spawn(async move {
392 let mut ticker = tokio::time::interval(READINESS_POLL);
393 let mut draining = false;
394 loop {
395 tokio::select! {
396 () = stop.cancelled() => return,
397 _ = ticker.tick() => {}
398 }
399 let now = state.probes().is_shutting_down();
400 if now != draining {
401 draining = now;
402 let status = if now {
403 ServingStatus::NotServing
404 } else {
405 ServingStatus::Serving
406 };
407 shared.set_health(status).await;
408 }
409 }
410 });
411}
412
413#[cfg(feature = "multiplex")]
418pub fn drain_on_autumn_shutdown(shared: &Arc<Shared>, state: &AppState) {
419 let shared = shared.clone();
420 let autumn = state.shutdown_token();
421 tokio::spawn(async move {
422 tokio::select! {
423 () = shared.stop.cancelled() => {}
424 () = autumn.cancelled() => {
425 tracing::info!("Autumn is stopping its listener; draining gRPC calls");
426 shared.request_shutdown();
427 }
428 }
429 });
430}
431
432const READINESS_POLL: Duration = Duration::from_millis(250);
434
435fn bind(addr: SocketAddr) -> std::io::Result<tokio::net::TcpListener> {
438 let listener = std::net::TcpListener::bind(addr)?;
439 listener.set_nonblocking(true)?;
440 tokio::net::TcpListener::from_std(listener)
441}
442
443struct Accept {
451 listener: Arc<Mutex<Option<TcpIncoming>>>,
452 permits: Option<PollSemaphore>,
453 permit: Option<OwnedSemaphorePermit>,
454 kill: CancellationToken,
455}
456
457impl Accept {
458 fn new(listener: tokio::net::TcpListener, config: &GrpcConfig, shared: &Shared) -> Self {
459 let listener = Arc::new(Mutex::new(Some(
460 TcpIncoming::from(listener)
461 .with_nodelay(Some(config.tcp_nodelay))
462 .with_keepalive(config.tcp_keepalive()),
463 )));
464 let slot = listener.clone();
465 let stop = shared.stop.clone();
466 tokio::spawn(async move {
467 stop.cancelled().await;
468 let closed = slot.lock().unwrap_or_else(PoisonError::into_inner).take();
469 drop(closed);
470 });
471 Self {
472 listener,
473 permits: (config.max_connections > 0)
474 .then(|| PollSemaphore::new(Arc::new(Semaphore::new(config.max_connections)))),
475 permit: None,
476 kill: shared.kill.clone(),
477 }
478 }
479}
480
481impl Stream for Accept {
482 type Item = std::io::Result<Killable>;
483
484 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
485 let this = self.get_mut();
486 if this.permit.is_none()
487 && let Some(permits) = this.permits.as_mut()
488 {
489 match permits.poll_acquire(cx) {
490 Poll::Ready(Some(permit)) => this.permit = Some(permit),
491 Poll::Ready(None) => return Poll::Ready(None),
493 Poll::Pending => return Poll::Pending,
494 }
495 }
496 let mut listener = this.listener.lock().unwrap_or_else(PoisonError::into_inner);
497 let Some(incoming) = listener.as_mut() else {
498 return Poll::Ready(None);
499 };
500 match incoming.poll_next_unpin(cx) {
501 Poll::Ready(Some(Ok(stream))) => Poll::Ready(Some(Ok(Killable::new(
502 stream,
503 &this.kill,
504 this.permit.take(),
505 )))),
506 Poll::Ready(Some(Err(error))) => Poll::Ready(Some(Err(error))),
507 Poll::Ready(None) => Poll::Ready(None),
508 Poll::Pending => Poll::Pending,
509 }
510 }
511}
512
513struct Killable {
516 inner: TcpStream,
517 kill: Pin<Box<WaitForCancellationFutureOwned>>,
518 _permit: Option<OwnedSemaphorePermit>,
520}
521
522impl Killable {
523 fn new(
526 inner: TcpStream,
527 kill: &CancellationToken,
528 permit: Option<OwnedSemaphorePermit>,
529 ) -> Self {
530 Self {
531 inner,
532 kill: Box::pin(kill.child_token().cancelled_owned()),
533 _permit: permit,
534 }
535 }
536
537 fn killed(&mut self, cx: &mut Context<'_>) -> bool {
538 self.kill.as_mut().poll(cx).is_ready()
539 }
540}
541
542fn aborted() -> std::io::Error {
543 std::io::Error::new(
544 std::io::ErrorKind::ConnectionAborted,
545 "gRPC shutdown grace period expired",
546 )
547}
548
549impl AsyncRead for Killable {
550 fn poll_read(
551 self: Pin<&mut Self>,
552 cx: &mut Context<'_>,
553 buf: &mut ReadBuf<'_>,
554 ) -> Poll<std::io::Result<()>> {
555 let this = self.get_mut();
556 if this.killed(cx) {
557 return Poll::Ready(Err(aborted()));
558 }
559 Pin::new(&mut this.inner).poll_read(cx, buf)
560 }
561}
562
563impl AsyncWrite for Killable {
564 fn poll_write(
565 self: Pin<&mut Self>,
566 cx: &mut Context<'_>,
567 buf: &[u8],
568 ) -> Poll<std::io::Result<usize>> {
569 let this = self.get_mut();
570 if this.killed(cx) {
571 return Poll::Ready(Err(aborted()));
572 }
573 Pin::new(&mut this.inner).poll_write(cx, buf)
574 }
575
576 fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
577 let this = self.get_mut();
578 if this.killed(cx) {
579 return Poll::Ready(Err(aborted()));
580 }
581 Pin::new(&mut this.inner).poll_flush(cx)
582 }
583
584 fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<std::io::Result<()>> {
585 Pin::new(&mut self.get_mut().inner).poll_shutdown(cx)
586 }
587}
588
589impl Connected for Killable {
590 type ConnectInfo = TcpConnectInfo;
591
592 fn connect_info(&self) -> Self::ConnectInfo {
593 self.inner.connect_info()
594 }
595}
596
597#[cfg(test)]
598#[allow(clippy::unwrap_used, clippy::panic)]
599mod tests {
600 use super::*;
601
602 #[test]
603 fn a_server_task_that_ends_while_serving_fails_and_releases_waiters() {
604 let shared = Arc::new(Shared::new());
605 shared.lifecycle.apply(LifecycleEvent::Bound).unwrap();
606 let stopped = shared.stopped.subscribe();
607 drop(ExitGuard(shared.clone()));
608 assert_eq!(shared.lifecycle.get(), Lifecycle::Failed);
609 assert!(*stopped.borrow(), "waiters are released");
610 }
611
612 #[test]
613 fn a_server_task_that_ends_while_draining_stops() {
614 let shared = Arc::new(Shared::new());
615 shared.lifecycle.apply(LifecycleEvent::Bound).unwrap();
616 shared
617 .lifecycle
618 .apply(LifecycleEvent::ShutdownRequested)
619 .unwrap();
620 drop(ExitGuard(shared.clone()));
621 assert_eq!(shared.lifecycle.get(), Lifecycle::Stopped);
622 }
623
624 #[tokio::test]
625 async fn a_panic_in_the_server_task_still_fails_the_lifecycle() {
626 let shared = Arc::new(Shared::new());
627 shared.lifecycle.apply(LifecycleEvent::Bound).unwrap();
628 let exit = ExitGuard(shared.clone());
629 let task = tokio::spawn(async move {
630 let _exit = exit;
631 panic!("server task panics");
632 });
633 assert!(task.await.is_err());
634 assert_eq!(shared.lifecycle.get(), Lifecycle::Failed);
635 }
636}