Skip to main content

autumn_plugin_grpc/
server.rs

1//! The gRPC listener: start, drain and stop. [`GrpcHandle`] is the control
2//! surface.
3
4use 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
31/// State shared by the plugin, its hooks and every [`GrpcHandle`].
32pub struct Shared {
33    pub lifecycle: LifecycleCell,
34    pub local_addr: OnceLock<SocketAddr>,
35    pub health: OnceLock<HealthReporter>,
36    /// Names reported by the health service (user services only).
37    pub health_names: OnceLock<Vec<String>>,
38    pub metrics: Arc<Metrics>,
39    /// The listener mode, set at start.
40    pub listener: OnceLock<Listener>,
41    /// Shared mode: the routes that the gate calls.
42    pub gate: OnceLock<crate::gate::Target>,
43    /// Shared mode: calls in flight through the gate.
44    pub calls: TaskTracker,
45    grace_ms: AtomicU64,
46    /// Ends the accept loop and starts tonic's graceful shutdown.
47    stop: CancellationToken,
48    /// Closes every open connection (dedicated) or call (shared). The
49    /// drain uses it after the grace period.
50    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    /// Set the status of `""` and each user service.
83    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    /// Start the drain task, once. It does not wait.
93    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            // From `Idle`: nothing to drain.
99            Ok(_) => self.mark_stopped(),
100            Err(_) => {}
101        }
102    }
103
104    /// The drain sequence. It runs in its own task, so it completes even
105    /// when Autumn drops a shutdown hook that runs too long.
106    async fn drain(self: Arc<Self>) {
107        self.set_health(ServingStatus::NotServing).await;
108        // Clear the statuses. This ends open `Watch` streams after they get
109        // `NOT_SERVING`, so they do not hold the drain open.
110        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        // Shared mode: Autumn owns the connections. Wait for the calls, then
137        // end the ones that are still open.
138        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
156/// Time for killed connections to close before the plugin aborts the task.
157pub const KILL_WAIT: Duration = Duration::from_secs(1);
158
159/// A handle to the gRPC server of one plugin.
160///
161/// Get it from [`GrpcPlugin::handle`](crate::GrpcPlugin::handle) before
162/// boot, or from `AppState` after boot:
163///
164/// ```rust,ignore
165/// let handle = state.extension::<GrpcHandle>().expect("gRPC plugin");
166/// ```
167///
168/// The plugin calls [`shutdown`](Self::shutdown) when the app stops. Call it
169/// yourself to drain earlier.
170#[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)] // `GrpcHandle` is public; this must not be.
186    pub(crate) const fn new(shared: Arc<Shared>) -> Self {
187        Self { shared }
188    }
189
190    /// The bound address. `None` before start, and always `None` with
191    /// `listener = "shared"`. With port `0`, this is the real port.
192    #[must_use]
193    pub fn local_addr(&self) -> Option<SocketAddr> {
194        self.shared.local_addr.get().copied()
195    }
196
197    /// The current lifecycle state.
198    #[must_use]
199    pub fn state(&self) -> Lifecycle {
200        self.shared.lifecycle.get()
201    }
202
203    /// The health reporter, when the health service is on. Use it to mark
204    /// a service `NOT_SERVING` at runtime.
205    #[must_use]
206    pub fn health_reporter(&self) -> Option<HealthReporter> {
207        self.shared.health.get().cloned()
208    }
209
210    /// The drain time that [`shutdown`](Self::shutdown) allows. It is
211    /// `shutdown_grace_ms`, capped by Autumn's `server.shutdown_timeout_secs`.
212    #[must_use]
213    pub fn shutdown_grace(&self) -> Duration {
214        self.shared.grace()
215    }
216
217    /// The current `grpc_server_*` metric families.
218    #[must_use]
219    pub fn metric_families(&self) -> Vec<MetricFamily> {
220        self.shared.metrics.families(self.state().is_ready())
221    }
222
223    /// Stop the server.
224    ///
225    /// 1. Set every health status to `NOT_SERVING`.
226    /// 2. Stop accepting connections. Tell clients to go away (HTTP/2
227    ///    `GOAWAY`).
228    /// 3. Wait for in-flight calls, up to the grace period.
229    /// 4. Close the connections that are still open.
230    ///
231    /// With `listener = "shared"`, Autumn owns the connections. Steps 2 and
232    /// 4 change: the plugin answers new calls with `UNAVAILABLE`, and it
233    /// ends open calls with `UNAVAILABLE`. The drain also starts by itself
234    /// when Autumn stops its listener.
235    ///
236    /// A task does the drain. If you drop this future, the drain continues.
237    /// You can call this method more than once, from many tasks. Each call
238    /// returns after the server stops.
239    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
247/// Everything [`start`] needs.
248pub 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
258/// Bind the listener and spawn the server task.
259pub 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    // tonic passes `None` to hyper, and `None` removes hyper's own limits.
279    // So always set the stream and reset limits.
280    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        // A shutdown came during start. Stop the new server too.
330        shared.stop.cancel();
331        return Err(GrpcError::NotIdle(state));
332    }
333    Ok(local_addr)
334}
335
336/// Shared mode: no listener. Make the routes available to the gate, then
337/// go to `Serving`.
338pub 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
369/// Applies `ServerExited` when the server task ends, also on a panic.
370struct 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
381/// Follow Autumn's readiness. When Autumn starts to shut down (or an
382/// operator drains it), report `NOT_SERVING` at once, so load balancers
383/// stop new calls before the drain. Report `SERVING` again if readiness
384/// comes back.
385pub 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/// Shared mode: start the drain when Autumn stops its listener. Autumn's
414/// HTTP drain waits for gRPC streams too, and its watchdog does not count
415/// them. Without this, an open stream (for example a health `Watch`)
416/// keeps Autumn from reaching the shutdown hooks.
417#[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
432/// How often the plugin reads Autumn's readiness.
433const READINESS_POLL: Duration = Duration::from_millis(250);
434
435/// Bind without awaiting, so a caller that cannot drive the reactor
436/// (for example `TestApp` hooks) does not block.
437fn 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
443/// The accept stream.
444///
445/// - A task drops the listener when `stop` fires. tonic stops polling this
446///   stream at that time, so the stream cannot do it itself. New clients
447///   then get "connection refused" during the drain.
448/// - With a connection limit, it takes a permit before each accept. The
449///   connection keeps the permit until it closes.
450struct 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                // The semaphore never closes. Treat a close as the end.
492                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
513/// A TCP stream that fails all I/O once `kill` fires. This closes
514/// connections that outlive the grace period.
515struct Killable {
516    inner: TcpStream,
517    kill: Pin<Box<WaitForCancellationFutureOwned>>,
518    /// Held until the connection closes (connection limit).
519    _permit: Option<OwnedSemaphorePermit>,
520}
521
522impl Killable {
523    /// A child token per connection keeps the lock of each poll local to
524    /// that connection.
525    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}