gioser
⎇ d6b8f18b43c9bd9bf7268e48b61392a2d62bb229 crates/ente-zero/src/graph/shutdown.rs Rust · 100 líneas · 3.6 KB Historia Quién tocó qué crudo

Pausa: 11 crates del fractal Ente #0 con cerebro completo d6b8f18b4 · Sergio ·

1//! Cascade shutdown: SIGTERM en orden topológico (hojas primero), grace
2//! period, SIGKILL para stragglers, reap final.
3
4use super::EnteGraph;
5use nix::errno::Errno;
6use nix::sys::signal::{kill, Signal};
7use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
8use nix::unistd::Pid;
9use std::time::{Duration, Instant};
10use tracing::{debug, info, warn};
11
12/// Tiempo que damos a los Entes tras SIGTERM antes de escalar a SIGKILL.
13pub const SHUTDOWN_GRACE: Duration = Duration::from_secs(2);
14
15impl EnteGraph {
16    pub async fn cascade_shutdown(&mut self) {
17        let order = self.topo_order();
18        let pids: Vec<Pid> = order.iter()
19            .filter_map(|id| self.incarnated.get(id).and_then(|i| i.pid))
20            .collect();
21
22        if pids.is_empty() {
23            info!("cascade shutdown: ningún Ente encarnado, salida limpia");
24            return;
25        }
26
27        info!(
28            count = pids.len(), grace_ms = SHUTDOWN_GRACE.as_millis() as u64,
29            "SIGTERM cascade (topológico, hojas primero)"
30        );
31        for pid in &pids {
32            match kill(*pid, Signal::SIGTERM) {
33                Ok(()) => {}
34                Err(Errno::ESRCH) => {} // ya muerto, lo cosecharemos abajo
35                Err(e) => warn!(?pid, ?e, "kill SIGTERM falló"),
36            }
37        }
38
39        let deadline = Instant::now() + SHUTDOWN_GRACE;
40        while Instant::now() < deadline {
41            if !self.incarnated.values().any(|i| i.pid.is_some()) {
42                break;
43            }
44            match waitpid(None, Some(WaitPidFlag::WNOHANG)) {
45                Ok(WaitStatus::Exited(pid, code)) => {
46                    self.reap_during_shutdown(pid);
47                    debug!(?pid, code, "reaped (exited)");
48                }
49                Ok(WaitStatus::Signaled(pid, sig, _)) => {
50                    self.reap_during_shutdown(pid);
51                    debug!(?pid, ?sig, "reaped (signaled)");
52                }
53                Ok(WaitStatus::StillAlive) | Err(Errno::EINTR) => {
54                    tokio::time::sleep(Duration::from_millis(50)).await;
55                }
56                Err(Errno::ECHILD) => return,
57                Ok(_) => {}
58                Err(e) => {
59                    warn!(?e, "waitpid fallo en shutdown grace");
60                    break;
61                }
62            }
63        }
64
65        let stragglers: Vec<Pid> = self.incarnated.values()
66            .filter_map(|i| i.pid)
67            .collect();
68
69        if stragglers.is_empty() {
70            info!("cascade shutdown completo (todos los Entes terminaron en gracia)");
71            return;
72        }
73
74        warn!(count = stragglers.len(), "stragglers post-SIGTERM, escalando a SIGKILL");
75        for pid in &stragglers {
76            let _ = kill(*pid, Signal::SIGKILL);
77        }
78        loop {
79            match waitpid(None, Some(WaitPidFlag::WNOHANG)) {
80                Ok(WaitStatus::Exited(pid, _)) | Ok(WaitStatus::Signaled(pid, _, _)) => {
81                    self.reap_during_shutdown(pid);
82                }
83                Ok(WaitStatus::StillAlive) => {
84                    std::thread::sleep(Duration::from_millis(20));
85                }
86                Err(Errno::ECHILD) => break,
87                _ => break,
88            }
89            if !self.incarnated.values().any(|i| i.pid.is_some()) { break; }
90        }
91        info!("cascade shutdown completo (con SIGKILL)");
92    }
93
94    fn reap_during_shutdown(&mut self, pid: Pid) {
95        let Some(id) = self.by_pid.remove(&pid.as_raw()) else { return };
96        if let Some(inc) = self.incarnated.remove(&id) {
97            self.unregister_provider(&inc.card);
98        }
99    }
100}