Skip to content

Signal handlers fail with compio::dispatcher::Dispatcher due to worker thread signal mask inheritance #658

@ortuman

Description

@ortuman

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+

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions