Description
When using compio::dispatcher::Dispatcher, compio_signal::ctrl_c() stops working reliably. Pressing Ctrl-C terminates the process immediately without giving the signal handler a chance to execute, making graceful shutdown impossible.
Root Cause
On Unix systems, when compio::dispatcher::Dispatcher spawns worker threads, they inherit the parent thread's signal mask. By default, SIGINT (Ctrl-C) and other signals can be delivered to any thread in the process. If a worker thread receives the signal before the async signal handler is polled on the main thread, the default signal handler runs (terminating the process) instead of the compio signal handler.
This is a well-known issue in multi-threaded Unix applications and requires explicit signal masking.
Reproduction
use compio::runtime::Runtime;
use compio::dispatcher::Dispatcher;
use compio_signal;
use std::num::NonZero;
fn main() {
let rt = Runtime::new().unwrap();
rt.block_on(async {
// Create dispatcher with worker threads
let dispatcher = Dispatcher::builder()
.worker_threads(NonZero::new(4).unwrap())
.build()
.unwrap();
println!("Press Ctrl-C...");
// This signal handler won't reliably execute
compio_signal::ctrl_c().await.unwrap();
println!("Received Ctrl-C!"); // This often doesn't print
dispatcher.join().await.ok();
});
}
Expected: "Received Ctrl-C!" prints when Ctrl-C is pressed.
Actual: Process terminates immediately without printing the message.
Workaround
Block SIGINT and SIGTERM in worker threads before creating the dispatcher:
#[cfg(unix)]
fn create_dispatcher_with_signal_masking(worker_threads: usize) -> anyhow::Result<RtDispatcher> {
use libc::{SIG_BLOCK, SIGINT, SIGTERM, pthread_sigmask, sigaddset, sigemptyset, sigset_t};
use std::num::NonZero;
// Block signals before creating worker threads
let old_mask = unsafe {
let mut new_mask: sigset_t = std::mem::zeroed();
sigemptyset(&mut new_mask);
sigaddset(&mut new_mask, SIGINT);
sigaddset(&mut new_mask, SIGTERM);
let mut old_mask: sigset_t = std::mem::zeroed();
pthread_sigmask(SIG_BLOCK, &new_mask, &mut old_mask);
old_mask
};
// Create dispatcher (worker threads inherit blocked signals)
let dispatcher = Dispatcher::builder()
.worker_threads(NonZero::new(worker_threads).unwrap())
.build()?;
// Restore signals in main thread
unsafe {
use libc::{SIG_SETMASK, pthread_sigmask};
pthread_sigmask(SIG_SETMASK, &old_mask, std::ptr::null_mut());
}
Ok(dispatcher)
}
Suggested Solutions
Option 1: Document the pattern
Add documentation and examples showing the signal masking pattern for production use cases with compio::dispatcher::Dispatcher.
Option 2: Handle internally
Add a with_signal_masking(bool) option to Dispatcher::builder() to handle this automatically:
let dispatcher = Dispatcher::builder()
.worker_threads(NonZero::new(4).unwrap())
.mask_signals(true) // Block SIGINT/SIGTERM in workers
.build()
.unwrap();
Environment
- OS: Linux 6.18.7
- compio version: 0.18.0
- Rust version: 1.85+
Description
When using
compio::dispatcher::Dispatcher,compio_signal::ctrl_c()stops working reliably. Pressing Ctrl-C terminates the process immediately without giving the signal handler a chance to execute, making graceful shutdown impossible.Root Cause
On Unix systems, when
compio::dispatcher::Dispatcherspawns worker threads, they inherit the parent thread's signal mask. By default, SIGINT (Ctrl-C) and other signals can be delivered to any thread in the process. If a worker thread receives the signal before the async signal handler is polled on the main thread, the default signal handler runs (terminating the process) instead of the compio signal handler.This is a well-known issue in multi-threaded Unix applications and requires explicit signal masking.
Reproduction
Expected: "Received Ctrl-C!" prints when Ctrl-C is pressed.
Actual: Process terminates immediately without printing the message.
Workaround
Block SIGINT and SIGTERM in worker threads before creating the dispatcher:
Suggested Solutions
Option 1: Document the pattern
Add documentation and examples showing the signal masking pattern for production use cases with
compio::dispatcher::Dispatcher.Option 2: Handle internally
Add a
with_signal_masking(bool)option toDispatcher::builder()to handle this automatically:Environment