Skip to main content

eggserve_core/server/
handle.rs

1//! Server lifecycle handle.
2//!
3//! A [`ServerHandle`] is returned by [`Server::start`] and provides control
4//! over the running server: readiness signaling, graceful/forced shutdown,
5//! and waiting for completion.
6//!
7//! # Lifecycle
8//!
9//! After `Server::start()` returns a handle, the caller should:
10//!
11//! 1. Call [`ServerHandle::ready`] to wait for the listener to be bound and
12//!    the accept loop to be running.
13//! 2. Use the server (make requests).
14//! 3. Call [`ServerHandle::shutdown`] to initiate graceful shutdown.
15//! 4. Call [`ServerHandle::wait`] to wait for all connections to drain.
16//!
17//! Dropping the handle triggers graceful shutdown (the server will stop
18//! accepting new connections and drain in-flight requests).
19//!
20//! # Thread safety
21//!
22//! All handle methods are safe to call from any thread. The handle is not
23//! `Clone` — there is exactly one handle per server instance. This prevents
24//! ambiguous shutdown semantics.
25
26use std::net::SocketAddr;
27use std::time::Duration;
28
29use tokio::sync::broadcast;
30
31use crate::server::errors::{ServerError, ShutdownResult};
32use crate::server::lifecycle::Lifecycle;
33
34/// Handle to a running server instance.
35///
36/// This type is experimental and its API may change without notice.
37///
38/// The handle allows the caller to:
39/// - Wait for readiness (via [`ServerHandle::ready`])
40/// - Trigger graceful shutdown (via [`ServerHandle::shutdown`])
41/// - Trigger forced shutdown (via [`ServerHandle::force_shutdown`])
42/// - Query the listening address (via [`ServerHandle::local_addr`])
43/// - Wait for completion (via [`ServerHandle::wait`])
44///
45/// Dropping the handle triggers graceful shutdown — the server stops
46/// accepting new connections and drains in-flight requests.
47pub struct ServerHandle {
48    local_addr: SocketAddr,
49    shutdown_tx: broadcast::Sender<()>,
50    join: Option<tokio::task::JoinHandle<ShutdownResult>>,
51    lifecycle: std::sync::Arc<Lifecycle>,
52}
53
54impl std::fmt::Debug for ServerHandle {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        f.debug_struct("ServerHandle")
57            .field("local_addr", &self.local_addr)
58            .field("state", &self.lifecycle.state())
59            .finish()
60    }
61}
62
63impl ServerHandle {
64    pub(crate) fn new(
65        local_addr: SocketAddr,
66        shutdown_tx: broadcast::Sender<()>,
67        join: tokio::task::JoinHandle<ShutdownResult>,
68        lifecycle: std::sync::Arc<Lifecycle>,
69    ) -> Self {
70        Self {
71            local_addr,
72            shutdown_tx,
73            join: Some(join),
74            lifecycle,
75        }
76    }
77
78    /// Returns the address the server is listening on.
79    ///
80    /// Useful when binding to port 0 to discover the actual port.
81    pub fn local_addr(&self) -> SocketAddr {
82        self.local_addr
83    }
84
85    /// Returns the current lifecycle state.
86    pub fn state(&self) -> crate::server::lifecycle::LifecycleState {
87        self.lifecycle.state()
88    }
89
90    /// Wait for the server to be ready to accept connections.
91    ///
92    /// This returns once the listener is bound and the accept loop has been
93    /// polled. After this returns, the server will accept new connections.
94    ///
95    /// If the server fails during startup, this returns an error.
96    ///
97    /// # State behavior
98    ///
99    /// - `Running`: immediate success (already ready)
100    /// - `Starting`: waits for transition to `Running` or `Failed`
101    /// - `Failed`: returns startup error
102    /// - `Created`: returns not-started error
103    /// - `Draining`/`Stopped`: returns not-running error
104    pub async fn ready(&self) -> Result<(), ServerError> {
105        let state = self.lifecycle.state();
106        match state {
107            crate::server::lifecycle::LifecycleState::Running => Ok(()),
108            crate::server::lifecycle::LifecycleState::Starting => {
109                self.lifecycle.wait_ready().await;
110
111                // Re-check after waiting.
112                let state = self.lifecycle.state();
113                match state {
114                    crate::server::lifecycle::LifecycleState::Running => Ok(()),
115                    crate::server::lifecycle::LifecycleState::Failed => {
116                        Err(ServerError::Startup("server failed during startup".into()))
117                    }
118                    other => Err(ServerError::Config(format!(
119                        "unexpected state after ready: {other}"
120                    ))),
121                }
122            }
123            crate::server::lifecycle::LifecycleState::Failed => {
124                Err(ServerError::Startup("server failed during startup".into()))
125            }
126            other => Err(ServerError::Config(format!(
127                "server not ready: in {other} state"
128            ))),
129        }
130    }
131
132    /// Trigger graceful shutdown.
133    ///
134    /// The server will stop accepting new connections and wait for in-flight
135    /// requests to complete (up to the configured grace period).
136    ///
137    /// Multiple calls are idempotent — only the first call has an effect.
138    pub fn shutdown(&self) {
139        // Transition to draining (idempotent — returns Ok for already-draining/stopped/created).
140        let _ = self.lifecycle.drain();
141        // Send broadcast signal to break accept loop.
142        let _ = self.shutdown_tx.send(());
143    }
144
145    /// Trigger forced shutdown with a deadline.
146    ///
147    /// Sends the shutdown signal and waits for the server to stop. If the
148    /// server does not stop within `deadline`, the accept task is aborted and
149    /// the server is marked stopped.
150    ///
151    /// Returns the [`ShutdownResult`] indicating how the shutdown completed.
152    pub async fn force_shutdown(
153        mut self,
154        deadline: Duration,
155    ) -> Result<ShutdownResult, ServerError> {
156        self.shutdown();
157        match tokio::time::timeout(deadline, self.wait_internal()).await {
158            Ok(()) => {
159                // Terminal state reached — await the join handle.
160                if let Some(join) = self.join.take() {
161                    match join.await {
162                        Ok(result) => Ok(result),
163                        Err(e) => Err(ServerError::Accept(std::io::Error::other(format!(
164                            "server task panicked: {}",
165                            e
166                        )))),
167                    }
168                } else {
169                    Ok(ShutdownResult::Clean)
170                }
171            }
172            Err(_deadline_exceeded) => {
173                if let Some(join) = self.join.take() {
174                    join.abort();
175                    let _ = join.await;
176                }
177                let _ = self.lifecycle.mark_stopped();
178                Ok(ShutdownResult::Forced)
179            }
180        }
181    }
182
183    /// Wait for the server to finish.
184    ///
185    /// This consumes the handle. If the server is still running, triggers
186    /// graceful shutdown first, then waits for all connections to drain.
187    /// Returns the [`ShutdownResult`] indicating how the shutdown completed.
188    pub async fn wait(mut self) -> Result<ShutdownResult, ServerError> {
189        // Trigger shutdown if still running.
190        let state = self.lifecycle.state();
191        if !state.is_terminal() {
192            self.shutdown();
193        }
194
195        // Wait for terminal state.
196        self.wait_internal().await;
197
198        // Await the join handle.
199        if let Some(join) = self.join.take() {
200            match join.await {
201                Ok(result) => Ok(result),
202                Err(e) => Err(ServerError::Accept(std::io::Error::other(format!(
203                    "server task panicked: {}",
204                    e
205                )))),
206            }
207        } else {
208            Ok(ShutdownResult::Clean)
209        }
210    }
211
212    /// Internal wait implementation.
213    async fn wait_internal(&self) {
214        // Subscribe to terminal state.
215        let mut terminal_rx = self.lifecycle.subscribe_terminal();
216        let state = self.lifecycle.state();
217        if state.is_terminal() {
218            return;
219        }
220        let _ = terminal_rx.recv().await;
221    }
222}
223
224impl Drop for ServerHandle {
225    fn drop(&mut self) {
226        // If the handle is dropped without explicit shutdown, trigger graceful shutdown.
227        if self.join.is_some() {
228            let _ = self.lifecycle.drain();
229            let _ = self.shutdown_tx.send(());
230        }
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use crate::server::lifecycle::Lifecycle;
238    use std::sync::Arc;
239
240    async fn make_test_handle() -> ServerHandle {
241        let lifecycle = Arc::new(Lifecycle::new());
242        let (tx, _rx) = broadcast::channel(1);
243        let join = tokio::spawn(async { ShutdownResult::Clean });
244        ServerHandle::new("127.0.0.1:8000".parse().unwrap(), tx, join, lifecycle)
245    }
246
247    fn make_handle_with_state(state: crate::server::lifecycle::LifecycleState) -> ServerHandle {
248        let lifecycle = Arc::new(Lifecycle::new());
249        match state {
250            crate::server::lifecycle::LifecycleState::Created => {}
251            crate::server::lifecycle::LifecycleState::Starting => {
252                lifecycle.start().unwrap();
253            }
254            crate::server::lifecycle::LifecycleState::Running => {
255                lifecycle.start().unwrap();
256                lifecycle.mark_running().unwrap();
257            }
258            crate::server::lifecycle::LifecycleState::Failed => {
259                lifecycle.mark_failed().unwrap();
260            }
261            crate::server::lifecycle::LifecycleState::Draining => {
262                lifecycle.start().unwrap();
263                lifecycle.mark_running().unwrap();
264                lifecycle.drain().unwrap();
265            }
266            crate::server::lifecycle::LifecycleState::Stopped => {
267                lifecycle.start().unwrap();
268                lifecycle.mark_running().unwrap();
269                lifecycle.drain().unwrap();
270                lifecycle.mark_stopped().unwrap();
271            }
272        }
273        let (shutdown_tx, _) = broadcast::channel(1);
274        let join = tokio::spawn(async { ShutdownResult::Clean });
275        ServerHandle::new("127.0.0.1:0".parse().unwrap(), shutdown_tx, join, lifecycle)
276    }
277
278    #[tokio::test]
279    async fn handle_local_addr() {
280        let handle = make_test_handle().await;
281        assert_eq!(
282            handle.local_addr(),
283            "127.0.0.1:8000".parse::<SocketAddr>().unwrap()
284        );
285    }
286
287    #[tokio::test]
288    async fn handle_state_initial() {
289        let handle = make_test_handle().await;
290        assert_eq!(
291            handle.state(),
292            crate::server::lifecycle::LifecycleState::Created
293        );
294    }
295
296    #[tokio::test]
297    async fn handle_shutdown_sends_signal() {
298        let lifecycle = Arc::new(Lifecycle::new());
299        // Transition to Running so drain works.
300        lifecycle.start().unwrap();
301        lifecycle.mark_running().unwrap();
302
303        let (tx, mut rx) = broadcast::channel(1);
304        let join = tokio::spawn(async move {
305            let _ = rx.recv().await;
306            ShutdownResult::Clean
307        });
308        let handle = ServerHandle::new("127.0.0.1:0".parse().unwrap(), tx, join, lifecycle);
309        handle.shutdown();
310        // The task should complete after receiving the shutdown signal.
311    }
312
313    #[tokio::test]
314    async fn handle_ready_returns_error_for_failed() {
315        let lifecycle = Arc::new(Lifecycle::new());
316        lifecycle.mark_failed().unwrap();
317
318        let (tx, _rx) = broadcast::channel(1);
319        let join = tokio::spawn(async { ShutdownResult::Clean });
320        let handle = ServerHandle::new("127.0.0.1:0".parse().unwrap(), tx, join, lifecycle);
321
322        let result = handle.ready().await;
323        assert!(result.is_err());
324    }
325
326    #[tokio::test]
327    async fn handle_debug_format() {
328        let handle = make_test_handle().await;
329        let debug = format!("{:?}", handle);
330        assert!(debug.contains("ServerHandle"));
331        assert!(debug.contains("127.0.0.1:8000"));
332    }
333
334    // --- Readiness correctness regression tests (Plan 121, Track C) ---
335
336    #[tokio::test]
337    async fn ready_already_running_returns_ok() {
338        let lifecycle = Arc::new(Lifecycle::new());
339        lifecycle.start().unwrap();
340        lifecycle.mark_running().unwrap();
341        assert_eq!(
342            lifecycle.state(),
343            crate::server::lifecycle::LifecycleState::Running
344        );
345
346        let (tx, _rx) = broadcast::channel(1);
347        let join = tokio::spawn(async { ShutdownResult::Clean });
348        let handle = ServerHandle::new("127.0.0.1:0".parse().unwrap(), tx, join, lifecycle);
349
350        let result = handle.ready().await;
351        assert!(
352            result.is_ok(),
353            "ready() on already-Running server: {:?}",
354            result.err()
355        );
356    }
357
358    #[tokio::test]
359    async fn ready_failed_returns_error() {
360        let handle = make_handle_with_state(crate::server::lifecycle::LifecycleState::Failed);
361        let result = handle.ready().await;
362        assert!(result.is_err());
363    }
364
365    #[tokio::test]
366    async fn ready_starting_then_running_succeeds() {
367        let lifecycle = Arc::new(Lifecycle::new());
368        lifecycle.start().unwrap();
369        let (tx, _) = broadcast::channel(1);
370        let join = tokio::spawn(async { ShutdownResult::Clean });
371        let handle = ServerHandle::new("127.0.0.1:0".parse().unwrap(), tx, join, lifecycle.clone());
372
373        // Transition to Running after a short delay.
374        tokio::spawn(async move {
375            tokio::time::sleep(Duration::from_millis(20)).await;
376            lifecycle.mark_running().unwrap();
377        });
378
379        let result = tokio::time::timeout(Duration::from_secs(5), handle.ready()).await;
380        assert!(result.is_ok());
381        assert!(result.unwrap().is_ok());
382    }
383
384    #[tokio::test]
385    async fn ready_starting_then_failed_returns_error() {
386        let lifecycle = Arc::new(Lifecycle::new());
387        lifecycle.start().unwrap();
388        let (tx, _) = broadcast::channel(1);
389        let join = tokio::spawn(async { ShutdownResult::Clean });
390        let handle = ServerHandle::new("127.0.0.1:0".parse().unwrap(), tx, join, lifecycle.clone());
391
392        // Transition to Failed after a short delay.
393        tokio::spawn(async move {
394            tokio::time::sleep(Duration::from_millis(20)).await;
395            lifecycle.mark_failed().unwrap();
396        });
397
398        let result = handle.ready().await;
399        assert!(result.is_err());
400    }
401
402    #[tokio::test]
403    async fn ready_stuck_starting_times_out() {
404        let handle = make_handle_with_state(crate::server::lifecycle::LifecycleState::Starting);
405        let result = tokio::time::timeout(Duration::from_millis(50), handle.ready()).await;
406        // Timeout fires; ready() was still awaiting.
407        assert!(result.is_err());
408    }
409
410    #[tokio::test]
411    async fn ready_draining_is_error() {
412        let handle = make_handle_with_state(crate::server::lifecycle::LifecycleState::Draining);
413        let result = handle.ready().await;
414        assert!(result.is_err());
415    }
416
417    #[tokio::test]
418    async fn ready_stopped_is_error() {
419        let handle = make_handle_with_state(crate::server::lifecycle::LifecycleState::Stopped);
420        let result = handle.ready().await;
421        assert!(result.is_err());
422    }
423
424    #[tokio::test]
425    async fn ready_idempotent_on_running() {
426        let handle = make_handle_with_state(crate::server::lifecycle::LifecycleState::Running);
427        // Call ready() twice — both should succeed immediately.
428        let r1 = tokio::time::timeout(Duration::from_millis(50), handle.ready()).await;
429        assert!(r1.is_ok() && r1.unwrap().is_ok());
430        // Re-use requires a new handle (ready takes &self, but we can call again).
431        let r2 = tokio::time::timeout(Duration::from_millis(50), handle.ready()).await;
432        assert!(r2.is_ok() && r2.unwrap().is_ok());
433    }
434}