OXYGENHUB

Coverage / crates/hub/src/shutdown.rs 80.0%

36 of 45 lines run, 12 of 22 functions · run #524 on main · run missed

1
//! Stopping without cutting anyone off (oxygenhub#233). On SIGTERM (a
2
//! `systemctl restart`) or Ctrl-C the hub stops taking connections, ends
3
//! its event streams (browsers reconnect by themselves), and gives the
4
//! requests in flight up to `DRAIN` to finish. Given a listening socket by
5
//! systemd (socket activation, docs/deploy.md), it serves that rather than
6
//! binding: the socket outlives the restart, so a request that arrives
7
//! between the old process and the new one waits in its queue instead of
8
//! being refused.
9
10
use std::sync::OnceLock;
11
use tokio::sync::watch;
12
13
/// How long the requests in flight get to finish.
14
pub const DRAIN: std::time::Duration = std::time::Duration::from_secs(30);
15
16
/// How long, after the drain, work still running on the runtime's
17
/// blocking threads (a slow request's repository work, a bucket call) may
18
/// hold the process. Dropping the runtime would wait for it however long
19
/// it takes: on 2026-10-17 a restart stood a minute past "stopped", until
20
/// systemd killed it.
21
pub const EXIT_GRACE: std::time::Duration = std::time::Duration::from_secs(1);
22
23
/// End the runtime once `serve` has returned, cutting off what still runs.
241×
pub fn finish(rt: tokio::runtime::Runtime) {
251×
    rt.shutdown_timeout(EXIT_GRACE);
261×
}
27
287×
fn sender() -> &'static watch::Sender<bool> {
29
    static STOP: OnceLock<watch::Sender<bool>> = OnceLock::new();
307×
    STOP.get_or_init(|| watch::channel(false).0)
317×
}
32
33
/// Begin stopping.
341×
pub fn begin() {
351×
    sender().send_replace(true);
361×
}
37
381×
pub fn is_stopping() -> bool {
391×
    *sender().borrow()
401×
}
41
42
/// Done when the hub begins stopping: for event streams to end on, and for
43
/// the servers' graceful shutdown.
445×
pub async fn stopping() {
455×
    let mut rx = sender().subscribe();
465×
    let _ = rx.wait_for(|s| *s).await;
472×
}
48
49
/// Wait for SIGTERM or Ctrl-C, then begin stopping. Installed before the
50
/// servers start, so a signal never kills the process outright.
511×
pub fn on_signal() -> anyhow::Result<impl std::future::Future<Output = ()>> {
52
    #[cfg(unix)]
531×
    let mut term = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
541×
    Ok(async move {
55
        #[cfg(unix)]
561×
        tokio::select! {
571×
            _ = term.recv() => tracing::info!("SIGTERM: stopping (requests in flight may take {} s)", DRAIN.as_secs()),
581×
            _ = tokio::signal::ctrl_c() => tracing::info!("stopping"),
59
        }
60
        #[cfg(not(unix))]
61
        {
62
            let _ = tokio::signal::ctrl_c().await;
63
            tracing::info!("stopping");
64
        }
650×
        begin();
660×
    })
671×
}
68
69
/// The listening socket systemd passed (LISTEN_FDS, first descriptor 3),
70
/// when it passed one to this process.
711×
pub fn systemd_listener() -> Option<std::net::TcpListener> {
72
    #[cfg(unix)]
73
    {
74
        use std::os::fd::FromRawFd;
751×
        let pid: u32 = std::env::var("LISTEN_PID").ok()?.parse().ok()?;
760×
        let fds: u32 = std::env::var("LISTEN_FDS").ok()?.parse().ok()?;
770×
        if pid != std::process::id() || fds < 1 {
780×
            return None;
790×
        }
80
        // SAFETY: systemd hands this process descriptor 3 as an open,
81
        // listening socket (sd_listen_fds(3)); nothing else owns it.
820×
        let listener = unsafe { std::net::TcpListener::from_raw_fd(3) };
830×
        listener.set_nonblocking(true).ok()?;
840×
        Some(listener)
85
    }
86
    #[cfg(not(unix))]
87
    None
881×
}
89
90
#[cfg(test)]
91
mod tests {
92
    /// Blocking work still running when the hub stops does not hold the
93
    /// process: the runtime ends within its grace.
94
    #[test]
951×
    fn finishing_does_not_wait_for_blocking_work() {
961×
        let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap();
971×
        rt.spawn_blocking(|| std::thread::sleep(std::time::Duration::from_secs(30)));
981×
        let started = std::time::Instant::now();
991×
        super::finish(rt);
1001×
        assert!(started.elapsed() < super::EXIT_GRACE * 3, "{:?}", started.elapsed());
1011×
    }
102
}