You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Fix premature global idle when a parked Sync waiter becomes runnable (#195)
## Summary
`Pool::get_task` honoured the `pending_idle` latch by firing an idle
epoch **before** attempting to dequeue. When a parked external waiter
(e.g. a task blocked on a `Sync` group's token) becomes runnable and is
drained into the pool's queue, firing idle up-front dropped the pool's
active count and released its `active_pools` slot while a runnable task
was still queued — letting a **global idle epoch fire prematurely**.
This reorders idle-driven reactions and, with shutdown-on-idle, can
quiesce the powerplant while real work is still pending.
## How it manifested
It was found via the NUbots **Director**, which dispatches provider
reactions onto the default pool while still holding its `Sync` token.
The re-entrant provider parks as an external waiter (arming
`pending_idle`); on token release the parked task is drained into the
pool's queue (now runnable), but the stale latch fires idle first,
reordering the Director's idle-driven steps and (with shutdown-on-idle)
causing early quiescence/hangs.
## The fix
Consume the `pending_idle` latch **without** firing idle — its only job
is to *wake* the worker so it re-checks its queue. The existing
dequeue-first / `!got` path then decides correctly:
- a **drained-runnable** waiter is dequeued and run (no idle),
- a **still-parked** waiter leaves the queue empty so the `!got` branch
fires idle exactly as before (preserving the cross-pool idle-wake /
deadlock-break behaviour).
## Regression test
`tests/tests/dsl/IdleParkedSyncWaiter.cpp` reproduces the topology that
triggered the bug (a REALTIME reaction on a concurrency-1 pool holding a
`Sync` token while re-entering it, driven by deterministic
`on<Trigger<Step<N>>>` steps), generalised away from any
Director-specific naming. It **fails deterministically before the fix
and passes after**:
- Without fix: `PASS=0 FAIL=30`
- With fix: `PASS=30 FAIL=30`
Full local `ctest` suite passes.
0 commit comments