A task that found the per-URL tarball mem-cache slot InProgress released
the RwLock read guard and only then polled Notify::notified().
notify_waiters() stores no permit - it wakes only futures already
registered at that instant - so an owner that flipped the slot to
Available/Failed and notified inside that window left the waiter parked
forever. The window needs the owner to complete its flip on another
worker thread while the waiter is preempted between the guard drop and
its first poll, which is why it surfaced only as a rare hang on an
oversubscribed CI runner: pacquet-cli::add
save_prefix_empty_writes_exact_version (a 0.2s test) hung for over an
hour and wedged the Rust CI job at
https://github.com/pnpm/pnpm/actions/runs/29705938247.
The waiter path runs on every cold add/install: the resolve-time
prefetcher owns the fetch and the install pass waits on the same slot.
A detached prefetch task losing the same race instead pinned the
store-index writer channel open and hung the final writer_task.await -
same root cause, either direction.
Fix per the documented tokio pattern: pin the Notified future and
enable() it to register interest before re-checking the slot state,
awaiting only while the slot is still InProgress, in a loop.
The exact interleaving cannot be reproduced deterministically without
loom (it requires preempting the waiter mid-poll), so no regression
test accompanies the fix. Instead, CI gains guardrails so a recurrence
is a visible failure, not a wedged job: a workspace .config/nextest.toml
sets slow-timeout terminate-after = 5 (a test stuck past 300s is killed
and reported as TIMEOUT with output; ~3.5x headroom over the slowest
legitimate retry-ladder tests), and the Rust CI test job is capped at
timeout-minutes: 60 as a backstop for non-test hangs.