Skip to main content

netget/utils/
shutdown.rs

1//! Cooperative shutdown for **blocking** capture loops.
2//!
3//! Most protocols run their accept loop as a Tokio task, so `stop_server` cancelling the
4//! `JoinHandle` registered with [`AppState::register_server_task`] is enough: the task is
5//! dropped at its next await point and the socket goes with it.
6//!
7//! The packet-capture family (`arp`, `datalink`, `icmp`, `isis`) cannot work that way. Their
8//! loops are synchronous — `pcap::Capture::next_packet()` and `socket2::Socket::recv_from()`
9//! block a thread from `spawn_blocking`, and **`JoinHandle::abort()` cannot interrupt a
10//! blocking call**: Tokio has no way to unwind a thread parked in a syscall. Each of the four
11//! also dropped its handle outright and never registered it, so on a host with the privileges
12//! to actually run them (root, `/dev/bpf*` on macOS/BSD, `CAP_NET_RAW` on Linux) `stop_server`
13//! left the capture running until the process exited — still capturing, still feeding the LLM,
14//! while the operator believed it had stopped.
15//!
16//! [`StopSignal`] is the mechanism that fixes that. It is an `AtomicBool` the blocking loop
17//! polls between packets, plus a Tokio task that trips it when cancelled, so the *existing*
18//! `register_server_task` / `remove_server` plumbing drives it with no new API:
19//!
20//! ```ignore
21//! let stop = StopSignal::new();
22//! let stop_in_loop = stop.clone();
23//! tokio::task::spawn_blocking(move || {
24//!     loop {
25//!         if stop_in_loop.is_stopped() { break; }   // ← checked every iteration
26//!         match cap.next_packet() { /* … */ }
27//!     }
28//! });
29//! // …after the loop reports readiness:
30//! app_state.register_server_task(server_id, stop.park_task()).await;
31//! ```
32//!
33//! # Why the read timeout matters
34//!
35//! A poll between packets only runs when the blocking call returns. The four callers each
36//! bound that:
37//!
38//! * `arp`, `datalink`, `isis` open pcap with `.timeout(1000)`, so `next_packet()` returns
39//!   `Error::TimeoutExpired` at least once a second on an idle link — the loop then sees the
40//!   flag. **Shutdown latency is therefore up to ~1s.**
41//! * `icmp` puts its raw socket in non-blocking mode and sleeps 10ms on `WouldBlock`, so it
42//!   notices within ~10ms.
43//!
44//! Removing the pcap read timeout would make shutdown unbounded again on an idle interface.
45//! Do not.
46
47use std::sync::atomic::{AtomicBool, Ordering};
48use std::sync::Arc;
49
50/// A flag a blocking loop polls to learn that it must stop.
51///
52/// Cloning is cheap and every clone observes the same flag.
53#[derive(Clone, Debug)]
54pub struct StopSignal(Arc<AtomicBool>);
55
56impl StopSignal {
57    /// A fresh signal in the "keep running" state.
58    pub fn new() -> Self {
59        StopSignal(Arc::new(AtomicBool::new(false)))
60    }
61
62    /// Has someone asked the loop to stop?
63    ///
64    /// Call this once per loop iteration, *before* the blocking read as well as after it, so a
65    /// stop that arrives while the loop is between packets is not missed.
66    pub fn is_stopped(&self) -> bool {
67        self.0.load(Ordering::SeqCst)
68    }
69
70    /// Ask the loop to stop. Idempotent.
71    pub fn stop(&self) {
72        self.0.store(true, Ordering::SeqCst);
73    }
74
75    /// A Tokio task that parks forever and trips this signal when it is dropped.
76    ///
77    /// Hand the returned handle to [`AppState::register_server_task`]. `remove_server` /
78    /// `stop_server` abort it, Tokio drops the future, the guard's `Drop` sets the flag, and
79    /// the blocking loop exits at its next poll. That is the whole point: the blocking loop
80    /// becomes stoppable through the same registration every other protocol already uses,
81    /// with no per-protocol shutdown channel and no central knowledge of capture protocols.
82    ///
83    /// Dropping the handle without aborting merely detaches the task, which leaves the signal
84    /// untripped — exactly the behaviour of every other registered accept loop, and the reason
85    /// registering it is not optional.
86    pub fn park_task(&self) -> tokio::task::JoinHandle<()> {
87        let guard = StopGuard(self.0.clone());
88        tokio::spawn(async move {
89            // Moved into the future so cancellation drops it.
90            let _guard = guard;
91            std::future::pending::<()>().await;
92        })
93    }
94}
95
96impl Default for StopSignal {
97    fn default() -> Self {
98        Self::new()
99    }
100}
101
102/// Trips the flag on drop. Lives inside the parked task's future.
103struct StopGuard(Arc<AtomicBool>);
104
105impl Drop for StopGuard {
106    fn drop(&mut self) {
107        self.0.store(true, Ordering::SeqCst);
108    }
109}