Skip to content

kqueue backend: in-process waker does not interrupt kevent() poll #928

@paddor

Description

@paddor

On macOS (kqueue backend), a task woken by an in-process event (e.g. flume channel send) is not scheduled promptly when the runtime is blocked in kevent(). The waker notification does not interrupt the kernel poll, so the woken task only runs after an unrelated I/O event or timer fires. On the io_uring backend (Linux), the same scenario works correctly.

Repro:

use std::time::Duration;

#[compio::main]
async fn main() {
    let (tx, rx) = flume::bounded::<u32>(1);

    // Spawned task: wait for a message, then print it.
    compio::runtime::spawn(async move {
        let val = rx.recv_async().await.unwrap();
        println!("received: {val}");
    })
    .detach();

    // Let the spawned task park on recv_async.
    compio::time::sleep(Duration::from_millis(50)).await;

    // Send a message. flume wakes the spawned task's waker.
    tx.send(42).unwrap();

    // This sleep should let the spawned task run, but on macOS
    // the runtime enters kevent() without checking for ready tasks,
    // so the spawned task never runs until this timer fires.
    compio::time::sleep(Duration::from_millis(500)).await;

    // On Linux: "received: 42" prints almost instantly.
    // On macOS: "received: 42" prints after the full 500ms,
    //           or not at all if replaced with a future that
    //           blocks on the spawned task's output.
}

Replace the final sleep with a future that depends on the spawned task completing (e.g. waiting for data the spawned task would write to a socket), and the program deadlocks on macOS.

Expected behavior

When a task's Waker is invoked (by flume, tokio::sync, event-listener, or any other in-process notification), the runtime should break out of kevent() and schedule the woken task before re-entering the kernel poll.

io_uring backends typically use an eventfd registered with the submission queue for this purpose. The kqueue equivalent would be EVFILT_USER or a self-pipe.

Observed behavior

On macOS, the woken task is not scheduled until the next unrelated I/O completion or timer expiry causes kevent() to return. If no other event is pending, the woken task starves.

Environment

  • compio 0.19.0-rc.2 / compio-driver 0.12.0-rc.2
  • macOS 15.7.3
  • rustc 1.95.0 (59807616e 2026-04-14) (Homebrew)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdriver: pollingAbout the polling driverhelp wantedExtra attention is needed

    Type

    No fields configured for Bug.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions