Skip to content

wait_for_ops: close the pop()/in_progress idle-detection window #166

Description

@pathscale

Context

Found during the 2026-07-27 concurrency work while auditing PersistenceTask (src/persistence/task.rs); never observed in practice (theoretical, nanosecond-scale), never fixed. Filing so it does not get lost.

The window

wait_for_ops returns when all three are true: queue.len() == 0, analyzer_inner_wt.count() == 0, !analyzer_in_progress.

The engine task's idle transition is:

engine_progress_notify.notify_waiters();
task_analyzer_in_progress.store(false, Ordering::Release);
let res = Some(engine_queue.pop().await);   // blocks; pop decrements len BEFORE returning
task_analyzer_in_progress.store(true, Ordering::Release);

Between pop() returning (queue len already decremented to 0, op held in a local variable, not yet in the analyzer) and store(true), all three conditions can be simultaneously true. A wait_for_ops caller checking in exactly that window returns while one operation is still un-applied. If the caller then drops the runtime (test block_on scope ending), the op is lost; if it reads on-disk state, it reads stale state.

Mitigating factors: the window is a few instructions wide; the 0010 queue swap made len increment before enqueue on the push side, which errs conservatively; 100+ measured full-suite runs never hit it. But it is a real hole in the barrier semantics.

Suggested fix

Make the busy flag cover the pop itself — e.g. a Queue::pop_busy(&self, busy: &AtomicBool) that loops:

loop {
    busy.store(true, Release);
    if let Some(op) = self.immediate_pop() { return op; }
    busy.store(false, Release);
    self.notify.notified().await;
}

Then there is no instant where an op has left the queue while busy is false. (Registering notified() after the failed pop is the same lost-wakeup-safe pattern the current pop uses; tokio Notify stores a permit.)

Verification hints

Hard to hit natively; a loom-style or targeted test would inject a yield between pop and store. Otherwise verify by review plus the usual suite (all wait_for_ops-based tests, ~430 tests, 4s warm).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions