OXYGENHUB
# 9aee52d392a6 oxygenhub / crates / hub / src / shutdown.rs
2.7 KiB · Rust · last changed Restarts drain: SIGTERM stops taking connections and lets requests finish 20 days ago source blame history raw
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
fn sender() -> &'static watch::Sender<bool> {
17
    static STOP: OnceLock<watch::Sender<bool>> = OnceLock::new();
18
    STOP.get_or_init(|| watch::channel(false).0)
19
}
20
21
/// Begin stopping.
22
pub fn begin() {
23
    sender().send_replace(true);
24
}
25
26
pub fn is_stopping() -> bool {
27
    *sender().borrow()
28
}
29
30
/// Done when the hub begins stopping: for event streams to end on, and for
31
/// the servers' graceful shutdown.
32
pub async fn stopping() {
33
    let mut rx = sender().subscribe();
34
    let _ = rx.wait_for(|s| *s).await;
35
}
36
37
/// Wait for SIGTERM or Ctrl-C, then begin stopping. Installed before the
38
/// servers start, so a signal never kills the process outright.
39
pub fn on_signal() -> anyhow::Result<impl std::future::Future<Output = ()>> {
40
    #[cfg(unix)]
41
    let mut term = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
42
    Ok(async move {
43
        #[cfg(unix)]
44
        tokio::select! {
45
            _ = term.recv() => tracing::info!("SIGTERM: stopping (requests in flight may take {} s)", DRAIN.as_secs()),
46
            _ = tokio::signal::ctrl_c() => tracing::info!("stopping"),
47
        }
48
        #[cfg(not(unix))]
49
        {
50
            let _ = tokio::signal::ctrl_c().await;
51
            tracing::info!("stopping");
52
        }
53
        begin();
54
    })
55
}
56
57
/// The listening socket systemd passed (LISTEN_FDS, first descriptor 3),
58
/// when it passed one to this process.
59
pub fn systemd_listener() -> Option<std::net::TcpListener> {
60
    #[cfg(unix)]
61
    {
62
        use std::os::fd::FromRawFd;
63
        let pid: u32 = std::env::var("LISTEN_PID").ok()?.parse().ok()?;
64
        let fds: u32 = std::env::var("LISTEN_FDS").ok()?.parse().ok()?;
65
        if pid != std::process::id() || fds < 1 {
66
            return None;
67
        }
68
        // SAFETY: systemd hands this process descriptor 3 as an open,
69
        // listening socket (sd_listen_fds(3)); nothing else owns it.
70
        let listener = unsafe { std::net::TcpListener::from_raw_fd(3) };
71
        listener.set_nonblocking(true).ok()?;
72
        Some(listener)
73
    }
74
    #[cfg(not(unix))]
75
    None
76
}