Skip to main content

autumn_plugin_grpc/
lifecycle.rs

1//! The server lifecycle as a finite state machine.
2//!
3//! ```mermaid
4//! stateDiagram-v2
5//!     [*] --> Idle
6//!     Idle --> Serving: Bound
7//!     Idle --> Failed: StartFailed
8//!     Idle --> Stopped: ShutdownRequested
9//!     Serving --> Draining: ShutdownRequested
10//!     Serving --> Failed: ServerExited
11//!     Draining --> Stopped: Drained / ServerExited
12//!     Stopped --> [*]
13//!     Failed --> [*]
14//! ```
15//!
16//! [`Lifecycle::next`] is the only transition function. It is pure and
17//! total. Illegal events return `None` and do not change the state.
18
19use std::fmt;
20use std::sync::atomic::{AtomicU8, Ordering};
21
22/// The state of one gRPC server.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24#[non_exhaustive]
25pub enum Lifecycle {
26    /// Not started yet.
27    Idle,
28    /// The listener is bound and accepts calls.
29    Serving,
30    /// Shutdown started. No new connections. In-flight calls finish.
31    Draining,
32    /// The server stopped. Terminal.
33    Stopped,
34    /// The server could not start, or stopped unexpectedly. Terminal.
35    Failed,
36}
37
38/// An input to the state machine.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40#[non_exhaustive]
41pub enum LifecycleEvent {
42    /// The listener is bound.
43    Bound,
44    /// The server could not start (config, TLS, reflection or bind error).
45    StartFailed,
46    /// The app asked the server to stop.
47    ShutdownRequested,
48    /// All in-flight calls ended, or the grace period expired.
49    Drained,
50    /// The server task ended.
51    ServerExited,
52}
53
54impl Lifecycle {
55    /// The next state after `event`, or `None` if `event` is illegal here.
56    #[must_use]
57    pub const fn next(self, event: LifecycleEvent) -> Option<Self> {
58        use LifecycleEvent::{Bound, Drained, ServerExited, ShutdownRequested, StartFailed};
59        match (self, event) {
60            (Self::Idle, Bound) => Some(Self::Serving),
61            (Self::Serving, ShutdownRequested) => Some(Self::Draining),
62            (Self::Idle, StartFailed) | (Self::Serving, ServerExited) => Some(Self::Failed),
63            (Self::Idle, ShutdownRequested) | (Self::Draining, Drained | ServerExited) => {
64                Some(Self::Stopped)
65            }
66            _ => None,
67        }
68    }
69
70    /// `true` only in [`Serving`](Self::Serving).
71    #[must_use]
72    pub const fn is_ready(self) -> bool {
73        matches!(self, Self::Serving)
74    }
75
76    /// `true` in [`Stopped`](Self::Stopped) and [`Failed`](Self::Failed).
77    #[must_use]
78    pub const fn is_terminal(self) -> bool {
79        matches!(self, Self::Stopped | Self::Failed)
80    }
81
82    /// Lower-case name, as used in health details.
83    #[must_use]
84    pub const fn as_str(self) -> &'static str {
85        match self {
86            Self::Idle => "idle",
87            Self::Serving => "serving",
88            Self::Draining => "draining",
89            Self::Stopped => "stopped",
90            Self::Failed => "failed",
91        }
92    }
93
94    const fn to_u8(self) -> u8 {
95        match self {
96            Self::Idle => 0,
97            Self::Serving => 1,
98            Self::Draining => 2,
99            Self::Stopped => 3,
100            Self::Failed => 4,
101        }
102    }
103
104    const fn from_u8(value: u8) -> Self {
105        match value {
106            0 => Self::Idle,
107            1 => Self::Serving,
108            2 => Self::Draining,
109            3 => Self::Stopped,
110            _ => Self::Failed,
111        }
112    }
113}
114
115impl fmt::Display for Lifecycle {
116    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117        f.write_str(self.as_str())
118    }
119}
120
121/// A [`Lifecycle`] shared between threads. It changes only through
122/// [`Lifecycle::next`].
123#[derive(Debug)]
124pub struct LifecycleCell(AtomicU8);
125
126impl LifecycleCell {
127    /// A cell in [`Lifecycle::Idle`].
128    #[must_use]
129    pub const fn new() -> Self {
130        Self(AtomicU8::new(Lifecycle::Idle.to_u8()))
131    }
132
133    /// The current state.
134    #[must_use]
135    pub fn get(&self) -> Lifecycle {
136        Lifecycle::from_u8(self.0.load(Ordering::Acquire))
137    }
138
139    /// Apply `event` atomically.
140    ///
141    /// # Errors
142    ///
143    /// Returns the unchanged state when `event` is illegal in it.
144    pub fn apply(&self, event: LifecycleEvent) -> Result<Lifecycle, Lifecycle> {
145        let mut current = self.0.load(Ordering::Acquire);
146        loop {
147            let state = Lifecycle::from_u8(current);
148            let next = state.next(event).ok_or(state)?;
149            match self.0.compare_exchange_weak(
150                current,
151                next.to_u8(),
152                Ordering::AcqRel,
153                Ordering::Acquire,
154            ) {
155                Ok(_) => return Ok(next),
156                Err(actual) => current = actual,
157            }
158        }
159    }
160}
161
162impl Default for LifecycleCell {
163    fn default() -> Self {
164        Self::new()
165    }
166}