Skip to main content

eggserve_core/server/
lifecycle.rs

1//! Lifecycle state machine for the HTTP server.
2//!
3//! The server follows a strict state progression:
4//!
5//! ```text
6//! Created → Starting → Running → Draining → Stopped
7//!             ↓                    ↓
8//!          Failed               Failed
9//! ```
10//!
11//! # State transitions
12//!
13//! - **Created → Starting**: `Server::start()` is called
14//! - **Starting → Running**: listener bound, accept loop polled, readiness signaled
15//! - **Starting → Failed**: bind, configuration, or accept-loop startup failure
16//! - **Running → Draining**: `ServerHandle::shutdown()` is called
17//! - **Draining → Stopped**: all in-flight connections complete or deadline expires
18//! - **Draining → Failed**: fatal runtime error during drain
19//!
20//! # Allowed operations per state
21//!
22//! | State     | build | start | ready | shutdown | force_shutdown | wait |
23//! |-----------|-------|-------|-------|----------|---------------|------|
24//! | Created   | yes   | yes   | -     | stop     | stop          | err  |
25//! | Starting  | -     | err   | yes   | stop     | stop          | err  |
26//! | Running   | -     | err   | ok    | ok       | ok            | yes  |
27//! | Draining  | -     | err   | err   | idempot  | ok            | yes  |
28//! | Stopped   | -     | err   | err   | noop     | noop          | ok   |
29//! | Failed    | -     | err   | err   | noop     | noop          | err  |
30
31use std::sync::atomic::{AtomicU8, Ordering};
32
33use tokio::sync::{broadcast, watch};
34
35/// Server lifecycle states.
36///
37/// This type is experimental and its API may change without notice.
38///
39/// Each state is represented as a `u8` for atomic storage.
40#[repr(u8)]
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum LifecycleState {
43    /// Initial state after `ServerBuilder::build()`.
44    Created = 0,
45    /// `Server::start()` has been called; binding and accept-loop init in progress.
46    Starting = 1,
47    /// Listener bound, accept loop running, ready to accept connections.
48    Running = 2,
49    /// Shutdown requested; draining in-flight connections.
50    Draining = 3,
51    /// All connections drained; terminal state.
52    Stopped = 4,
53    /// Fatal error during startup or drain; terminal state.
54    Failed = 5,
55}
56
57impl LifecycleState {
58    /// Convert from raw atomic value.
59    fn from_u8(v: u8) -> Self {
60        match v {
61            0 => Self::Created,
62            1 => Self::Starting,
63            2 => Self::Running,
64            3 => Self::Draining,
65            4 => Self::Stopped,
66            5 => Self::Failed,
67            _ => Self::Failed,
68        }
69    }
70
71    /// Whether this is a terminal state (no further transitions expected).
72    pub fn is_terminal(self) -> bool {
73        matches!(self, Self::Stopped | Self::Failed)
74    }
75}
76
77impl std::fmt::Display for LifecycleState {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        match self {
80            Self::Created => write!(f, "created"),
81            Self::Starting => write!(f, "starting"),
82            Self::Running => write!(f, "running"),
83            Self::Draining => write!(f, "draining"),
84            Self::Stopped => write!(f, "stopped"),
85            Self::Failed => write!(f, "failed"),
86        }
87    }
88}
89
90/// Shared lifecycle state with atomic transitions and channel notifications.
91#[derive(Debug)]
92pub(crate) struct Lifecycle {
93    state: AtomicU8,
94    ready_tx: watch::Sender<bool>,
95    /// Notified when a terminal state (Stopped/Failed) is reached.
96    terminal_tx: broadcast::Sender<()>,
97}
98
99impl Default for Lifecycle {
100    fn default() -> Self {
101        Self::new()
102    }
103}
104
105impl Lifecycle {
106    /// Create a new lifecycle in the `Created` state.
107    pub(crate) fn new() -> Self {
108        let (ready_tx, _) = watch::channel(false);
109        let (terminal_tx, _) = broadcast::channel(1);
110        Self {
111            state: AtomicU8::new(LifecycleState::Created as u8),
112            ready_tx,
113            terminal_tx,
114        }
115    }
116
117    /// Get the current state.
118    pub(crate) fn state(&self) -> LifecycleState {
119        LifecycleState::from_u8(self.state.load(Ordering::Acquire))
120    }
121
122    /// Transition to `Starting`. Fails if not in `Created`.
123    pub(crate) fn start(&self) -> Result<(), crate::server::errors::ServerError> {
124        let prev = self.state.compare_exchange(
125            LifecycleState::Created as u8,
126            LifecycleState::Starting as u8,
127            Ordering::AcqRel,
128            Ordering::Acquire,
129        );
130        match prev {
131            Ok(_) => Ok(()),
132            Err(actual) => {
133                let state = LifecycleState::from_u8(actual);
134                if matches!(state, LifecycleState::Running | LifecycleState::Starting) {
135                    Err(crate::server::errors::ServerError::AlreadyStarted)
136                } else {
137                    Err(crate::server::errors::ServerError::Config(format!(
138                        "cannot start: server is in {} state",
139                        state
140                    )))
141                }
142            }
143        }
144    }
145
146    /// Transition to `Running`. Fails if not in `Starting`.
147    pub(crate) fn mark_running(&self) -> Result<(), crate::server::errors::ServerError> {
148        let prev = self.state.compare_exchange(
149            LifecycleState::Starting as u8,
150            LifecycleState::Running as u8,
151            Ordering::AcqRel,
152            Ordering::Acquire,
153        );
154        match prev {
155            Ok(_) => {
156                let _ = self.ready_tx.send(true);
157                Ok(())
158            }
159            Err(actual) => Err(crate::server::errors::ServerError::Config(format!(
160                "cannot mark running: server is in {} state",
161                LifecycleState::from_u8(actual)
162            ))),
163        }
164    }
165
166    /// Transition to `Draining`. Fails if not in `Running`.
167    pub(crate) fn drain(&self) -> Result<(), crate::server::errors::ServerError> {
168        let prev = self.state.compare_exchange(
169            LifecycleState::Running as u8,
170            LifecycleState::Draining as u8,
171            Ordering::AcqRel,
172            Ordering::Acquire,
173        );
174        match prev {
175            Ok(_) => {
176                crate::ops::Logger::global().emit(crate::ops::Event::new(
177                    crate::ops::Severity::Info,
178                    crate::ops::EventKind::DrainingStarted,
179                    "draining in-flight connections",
180                ));
181                Ok(())
182            }
183            Err(actual) => {
184                let state = LifecycleState::from_u8(actual);
185                if state == LifecycleState::Created || state == LifecycleState::Starting {
186                    // Make shutdown-before-start terminal so waiters cannot hang.
187                    if self
188                        .state
189                        .compare_exchange(
190                            actual,
191                            LifecycleState::Stopped as u8,
192                            Ordering::AcqRel,
193                            Ordering::Acquire,
194                        )
195                        .is_ok()
196                    {
197                        let _ = self.terminal_tx.send(());
198                        Ok(())
199                    } else {
200                        Err(crate::server::errors::ServerError::Config(
201                            "server state changed while shutting down".into(),
202                        ))
203                    }
204                } else if state.is_terminal() {
205                    Ok(())
206                } else {
207                    Err(crate::server::errors::ServerError::Config(format!(
208                        "cannot drain: server is in {} state",
209                        state
210                    )))
211                }
212            }
213        }
214    }
215
216    /// Transition to `Stopped`. Fails if not in `Draining`.
217    pub(crate) fn mark_stopped(&self) -> Result<(), crate::server::errors::ServerError> {
218        let prev = self.state.compare_exchange(
219            LifecycleState::Draining as u8,
220            LifecycleState::Stopped as u8,
221            Ordering::AcqRel,
222            Ordering::Acquire,
223        );
224        match prev {
225            Ok(_) => {
226                let _ = self.terminal_tx.send(());
227                Ok(())
228            }
229            Err(actual) => {
230                let state = LifecycleState::from_u8(actual);
231                if state.is_terminal() {
232                    Ok(())
233                } else {
234                    Err(crate::server::errors::ServerError::Config(format!(
235                        "cannot stop: server is in {} state",
236                        state
237                    )))
238                }
239            }
240        }
241    }
242
243    /// Transition to `Failed` from any non-terminal state.
244    ///
245    /// Signals both the terminal and ready channels so that any waiters
246    /// (including [`Self::wait_ready`]) are unblocked.
247    #[allow(dead_code)]
248    pub(crate) fn mark_failed(&self) -> Result<(), crate::server::errors::ServerError> {
249        let current = self.state.load(Ordering::Acquire);
250        let current_state = LifecycleState::from_u8(current);
251        if current_state.is_terminal() {
252            return Ok(());
253        }
254        self.state
255            .store(LifecycleState::Failed as u8, Ordering::Release);
256        // Signal both channels: terminal for shutdown waiters, ready for
257        // readiness waiters (they will re-check state and see Failed).
258        let _ = self.ready_tx.send(true);
259        let _ = self.terminal_tx.send(());
260        Ok(())
261    }
262
263    /// Wait for readiness (transition to `Running`).
264    pub(crate) async fn wait_ready(&self) {
265        let mut rx = self.ready_tx.subscribe();
266        // If already ready, return immediately.
267        if *rx.borrow() {
268            return;
269        }
270        let _ = rx.changed().await;
271    }
272
273    /// Subscribe to terminal state notifications.
274    pub(crate) fn subscribe_terminal(&self) -> broadcast::Receiver<()> {
275        self.terminal_tx.subscribe()
276    }
277
278    /// Check if the state matches the expected state.
279    #[allow(dead_code)]
280    pub(crate) fn is(&self, expected: LifecycleState) -> bool {
281        self.state() == expected
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn initial_state_is_created() {
291        let lc = Lifecycle::new();
292        assert_eq!(lc.state(), LifecycleState::Created);
293        assert!(!lc.state().is_terminal());
294    }
295
296    #[test]
297    fn valid_transitions() {
298        let lc = Lifecycle::new();
299        assert!(lc.start().is_ok());
300        assert_eq!(lc.state(), LifecycleState::Starting);
301
302        assert!(lc.mark_running().is_ok());
303        assert_eq!(lc.state(), LifecycleState::Running);
304
305        assert!(lc.drain().is_ok());
306        assert_eq!(lc.state(), LifecycleState::Draining);
307
308        assert!(lc.mark_stopped().is_ok());
309        assert_eq!(lc.state(), LifecycleState::Stopped);
310        assert!(lc.state().is_terminal());
311    }
312
313    #[test]
314    fn double_start_fails() {
315        let lc = Lifecycle::new();
316        assert!(lc.start().is_ok());
317        assert!(lc.mark_running().is_ok());
318        let err = lc.start().unwrap_err();
319        assert!(err.to_string().contains("already started"));
320    }
321
322    #[test]
323    fn shutdown_before_start_stops_lifecycle() {
324        let lc = Lifecycle::new();
325        assert!(lc.drain().is_ok());
326        assert_eq!(lc.state(), LifecycleState::Stopped);
327    }
328
329    #[test]
330    fn mark_failed_from_any_non_terminal() {
331        let lc = Lifecycle::new();
332        assert!(lc.mark_failed().is_ok());
333        assert_eq!(lc.state(), LifecycleState::Failed);
334        assert!(lc.state().is_terminal());
335    }
336
337    #[test]
338    fn mark_stopped_from_already_stopped_is_ok() {
339        let lc = Lifecycle::new();
340        assert!(lc.start().is_ok());
341        assert!(lc.mark_running().is_ok());
342        assert!(lc.drain().is_ok());
343        assert!(lc.mark_stopped().is_ok());
344        assert!(lc.mark_stopped().is_ok());
345    }
346
347    #[test]
348    fn lifecycle_state_display() {
349        assert_eq!(LifecycleState::Created.to_string(), "created");
350        assert_eq!(LifecycleState::Running.to_string(), "running");
351        assert_eq!(LifecycleState::Draining.to_string(), "draining");
352        assert_eq!(LifecycleState::Stopped.to_string(), "stopped");
353        assert_eq!(LifecycleState::Failed.to_string(), "failed");
354    }
355}