seq_runtime/
diagnostics.rs

1//! Runtime diagnostics for production debugging
2//!
3//! Provides a SIGQUIT (kill -3) handler that dumps runtime statistics to stderr,
4//! similar to JVM thread dumps. This is useful for debugging production issues
5//! without stopping the process.
6//!
7//! ## Usage
8//!
9//! Send SIGQUIT to a running Seq process:
10//! ```bash
11//! kill -3 <pid>
12//! ```
13//!
14//! The process will dump diagnostics to stderr and continue running.
15//!
16//! ## Signal Safety
17//!
18//! Signal handlers can only safely call async-signal-safe functions. Our
19//! dump_diagnostics() does I/O and acquires locks, which is NOT safe to call
20//! directly from a signal handler. Instead, we spawn a dedicated thread that
21//! waits for signals using signal-hook's iterator API, making all the I/O
22//! operations safe.
23
24use crate::scheduler::ACTIVE_STRANDS;
25use std::sync::Once;
26use std::sync::atomic::Ordering;
27
28static SIGNAL_HANDLER_INIT: Once = Once::new();
29
30/// Install the SIGQUIT signal handler for diagnostics
31///
32/// This is called automatically by scheduler_init, but can be called
33/// explicitly if needed. Safe to call multiple times (idempotent).
34///
35/// # Implementation
36///
37/// Uses a dedicated thread to handle signals safely. The signal-hook iterator
38/// API ensures we're not calling non-async-signal-safe functions from within
39/// a signal handler context.
40pub fn install_signal_handler() {
41    SIGNAL_HANDLER_INIT.call_once(|| {
42        #[cfg(unix)]
43        {
44            use signal_hook::consts::SIGQUIT;
45            use signal_hook::iterator::Signals;
46
47            // Create signal iterator - this is safe and doesn't block
48            let mut signals = match Signals::new([SIGQUIT]) {
49                Ok(s) => s,
50                Err(_) => return, // Silently fail if we can't register
51            };
52
53            // Spawn a dedicated thread to handle signals
54            // This thread blocks waiting for signals, then safely calls dump_diagnostics()
55            std::thread::Builder::new()
56                .name("seq-diagnostics".to_string())
57                .spawn(move || {
58                    for sig in signals.forever() {
59                        if sig == SIGQUIT {
60                            dump_diagnostics();
61                        }
62                    }
63                })
64                .ok(); // Silently fail if thread spawn fails
65        }
66
67        #[cfg(not(unix))]
68        {
69            // Signal handling not supported on non-Unix platforms
70            // Diagnostics can still be called directly via dump_diagnostics()
71        }
72    });
73}
74
75/// Dump runtime diagnostics to stderr
76///
77/// This can be called directly from code or triggered via SIGQUIT.
78/// Output goes to stderr to avoid mixing with program output.
79pub fn dump_diagnostics() {
80    use std::io::Write;
81
82    let mut out = std::io::stderr().lock();
83
84    let _ = writeln!(out, "\n=== Seq Runtime Diagnostics ===");
85    let _ = writeln!(out, "Timestamp: {:?}", std::time::SystemTime::now());
86
87    // Strand count (global atomic - accurate)
88    let active = ACTIVE_STRANDS.load(Ordering::Relaxed);
89    let _ = writeln!(out, "\n[Strands]");
90    let _ = writeln!(out, "  Active: {}", active);
91
92    // Channel stats (global registry - accurate if lock available)
93    let _ = writeln!(out, "\n[Channels]");
94    match get_channel_count() {
95        Some(count) => {
96            let _ = writeln!(out, "  Open channels: {}", count);
97        }
98        None => {
99            let _ = writeln!(out, "  Open channels: (unavailable - registry locked)");
100        }
101    }
102
103    let _ = writeln!(out, "\n=== End Diagnostics ===\n");
104}
105
106/// Try to get channel count without blocking
107/// Returns None if the registry lock is held
108fn get_channel_count() -> Option<usize> {
109    use crate::channel::channel_count;
110    channel_count()
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_dump_diagnostics_runs() {
119        // Just verify it doesn't panic
120        dump_diagnostics();
121    }
122
123    #[test]
124    fn test_install_signal_handler_idempotent() {
125        // Should be safe to call multiple times
126        install_signal_handler();
127        install_signal_handler();
128        install_signal_handler();
129    }
130}