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}