rust-lang/rust · critical

non-empty waiters vec

Error message

non-empty waiters vec

What it means

`QueryLatch::extract_waiter` (job.rs:127) calls `.expect("non-empty waiters vec")` on `self.waiters` while breaking a query cycle. The waiter vector is only `take`n (emptied) when the latch is fully set; calling `extract_waiter` on an already-resolved latch violates the cycle-breaker's precondition. It is an invariant of the query deadlock detector, not a user-facing check.

Source

Thrown at compiler/rustc_middle/src/query/job.rs:127

        }
    }

    /// Sets the latch and resumes all waiters on it
    fn set(&self) {
        let mut waiters_guard = self.waiters.lock();
        let waiters = waiters_guard.take().unwrap(); // mark the latch as complete
        let registry = rustc_thread_pool::Registry::current();
        for waiter in waiters {
            rustc_thread_pool::mark_unblocked(&registry);
            waiter.condvar.notify_one();
        }
    }

    /// Removes a single waiter from the list of waiters.
    /// This is used to break query cycles.
    pub fn extract_waiter(&self, waiter: usize) -> Arc<QueryWaiter<'tcx>> {
        let mut waiters_guard = self.waiters.lock();
        let waiters = waiters_guard.as_mut().expect("non-empty waiters vec");
        // Remove the waiter from the list of waiters
        waiters.remove(waiter)
    }
}

View on GitHub (pinned to 22057b88b0)

Solutions

  1. Report to rust-lang/rust with `rustc -vV`, thread count, and a minimal reproducer (this is a race in the query system).
  2. Retry with a single thread: `RUST_MIN_STACK=... rustc -Z threads=1` or set `RUSTC_THREADS=1` via cargo to sidestep the race.
  3. Retry on the latest nightly; query-system races get fixed quickly.
  4. If building rustc itself, re-run `x.py build` after `cargo clean` to rule out a stale incremental interaction with the query scheduler.
Defensive patterns

Strategy: retry

Try / catch

// 'non-empty waiters vec' is an internal query-system invariant violation (ICE).
// Nothing in user code triggers it deterministically; treat as transient.
use std::process::Command;
fn compile_with_jitter(dir: &str) -> bool {
    for attempt in 0..3u8 {
        // Single-threaded rebuild avoids the concurrency edge case in the query scheduler.
        let ok = Command::new("cargo")
            .args(["build", "-j", "1"])
            .current_dir(dir)
            .status()
            .map(|s| s.success())
            .unwrap_or(false);
        if ok { return true; }
        let _ = Command::new("cargo").args(["clean"]).current_dir(dir).status();
    }
    false
}

Prevention

When it happens

Trigger: Reached only inside the rustc query system's cycle handler: when the deadlock detector decides to remove one specific waiter from a latch to break a cycle, but that latch's `waiters` field has already been `take`n—meaning another thread concurrently completed the query and drained the list. Indicates a race between latch completion and cycle extraction.

Common situations: Observed by rustc developers stress-testing parallel query evaluation (`-Z threads=N` with pathological dependency graphs), running the compiler under heavy load with very deep generic recursion, or testing nightly query-system changes. End-user Rust code cannot deliberately construct it; ordinary crashes here are rustc bugs.

Related errors


AI-assisted analysis of rust-lang/rust@22057b88b0 (2026-08-03). Data as JSON: /data/errors/06d2b41489337d32.json. Report an issue: GitHub.