diff --git a/src/io.rs b/src/io.rs index 4e8a82a..6fa8d1b 100644 --- a/src/io.rs +++ b/src/io.rs @@ -498,8 +498,12 @@ fn make_pipe() -> io::Result<(RawFd, RawFd)> { Ok((fds[0], fds[1])) } -/// Drain pending bytes from the wake pipe. The scheduler calls this after -/// a `poll` wakeup so the next idle call sees an empty pipe. +/// Drain pending bytes from the wake pipe. Nonblocking (pipe is O_NONBLOCK). +/// +/// DISCIPLINE: called only by the phase-1 drain-lock winner, immediately +/// before `drain_completions`. Bytes are the notification channel for +/// completions; consuming one anywhere else can strand the completion it +/// announces (see the lost-wakeup note at the call site in `schedule_loop`). pub fn drain_wake_pipe(fd: RawFd) { let mut buf = [0u8; 64]; loop { diff --git a/src/runtime.rs b/src/runtime.rs index c4a3891..3baf993 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1342,7 +1342,20 @@ fn schedule_loop(inner: &Arc, slot_idx: usize) { let completions = match inner.io.lock() { Ok(mut io) => io .as_mut() - .map(|io| io.drain_completions()) + .map(|io| { + // Consume wake-pipe bytes ONLY here, under the drain + // lock and strictly before draining completions. + // Producers push their completion before writing the + // byte, so every byte consumed here has its completion + // visible to the drain below. Consuming bytes anywhere + // else — in particular after an idle poll, outside the + // lock — loses wakeups: a try_lock loser can eat the + // byte for a completion the winner never saw, leaving + // it stranded (and its EPOLLONESHOT fd disarmed) until + // an unrelated timer forces another drain pass. + crate::io::drain_wake_pipe(io.wake_fd()); + io.drain_completions() + }) .unwrap_or_default(), Err(e) => panic!("smarm: io lock poisoned (core corrupt): {e}"), }; @@ -1552,16 +1565,22 @@ fn schedule_loop(inner: &Arc, slot_idx: usize) { let timeout = deadline - now; match fd_opt { Some(fd) => { + // Wake only; the byte (if any) is + // consumed by the next drain-lock + // winner in phase 1. Level-triggered + // poll means an unconsumed byte makes + // this return immediately, so a loser + // spins briefly until the winner + // releases — never sleeps through it. crate::io::poll_wake(fd, Some(timeout)); - crate::io::drain_wake_pipe(fd); } None => thread::sleep(timeout), } } } (None, Some(fd)) if io_outstanding > 0 => { + // See above: no byte consumption outside phase 1. crate::io::poll_wake(fd, None); - crate::io::drain_wake_pipe(fd); } _ => { thread::sleep(std::time::Duration::from_micros(100));