Priorities and aging
Classes
| Class | Base score |
|---|---|
Critical | 10,000 |
High | 1,000 |
Normal (default) | 100 |
Low | 10 |
Effective score
When a worker slot is free, the pool dispatches the ready task with the highest effective score:
score = base[class] + min(aging_rate[class] × wait_seconds, age_cap[class]) + urgency
with defaults aging_rate = 2.0 points per second and age_cap = 800 for every class.
urgency is zero unless the task has a deadline. Ties are broken by class, then
by submission order.
Aging
Aging prevents starvation: a task that keeps waiting slowly gains score until it overtakes newer work of a higher class.
With the defaults:
- A
Lowtask reaches10 + 800 = 810after 400 seconds. Against a continuous stream of freshNormalwork (score about100), it wins once10 + 2w > 100, that is after about 45 seconds of waiting. Normalcan reach900andHighcan reach1800. Neither crosses the next class's base score (1000and10000), so freshHighwork always beats agedNormalwork, andCriticalalways beats everything except deadline urgency.
This is deliberate: aging rescues background work from floods of ordinary work, but never lets
it delay urgent work. If you want stronger mixing, raise age_cap (Rust:
Builder::priority(PriorityConfig { ... })).
The time is measured from submission. A task released by its dependencies, or retried after a failure, keeps its original place in its class.
Why dispatch never rescans the backlog
Within one class, every task ages at the same rate from its own submission time, so an older task always has a score at least as high as a younger one. A FIFO queue per class therefore always has its best task at the head. Deadline tasks live in a second per-class queue ordered by latest start time. In pools with fair sharing, tasks without deadlines live in a per-class fair-share queue ordered by start tag instead of the FIFO lane. Dispatch compares at most eight heads (four classes × two queues), computing their scores lazily. There is no periodic rescoring and no full-queue scan, so choosing the next task costs at most the logarithm of the backlog. Fresh tasks join the back of their lane; a retried or woken task goes back to its place by submission order, which in a very long lane moves the entries behind it.
Measuring adherence
Each pool counts priority inversions: dispatches made while a ready task of a strictly
higher class was waiting in the same pool (tasks blocked on resources do not count).
priority_adherence = 1 − inversions / dispatches over the metrics window. With aging and
deadlines some inversions are intended; a sudden drop usually means an aging or deadline policy
is overriding priorities more often than expected.
Configuration
| Setting | Default | Where |
|---|---|---|
| Base scores | 10000 / 1000 / 100 / 10 | PriorityConfig::base |
| Aging rate | 2.0 points/s | PriorityConfig::aging_rate |
| Age cap | 800 | PriorityConfig::age_cap |
All three are arrays indexed by class (critical, high, normal, low).