Severity: correctness (timer ordering diverges from Node)
Found by: e2e A/B while working on #6084's timer drain (PR #6285).
What
Perry fires an expired timer batch in creation order. Node fires by deadline. So a shorter timer created later than a longer one fires second:
setTimeout(() => console.log("late10"), 10);
setTimeout(() => console.log("reffed"), 5); // created later, shorter deadline
- node:
reffed, late10
- perry:
late10, reffed
Reproduced with a mixed burst (10 same-deadline + 5 cleared + staggered 5/10/20/30 ms). Everything else in that test matches Node byte-for-byte — same-deadline timers do fire in creation order correctly, cancelled timers never fire, and hasRef() is right. The divergence is purely the ordering between different deadlines that are all expired at the same tick.
Why
js_timer_tick collects expired timers by walking TIMER_QUEUE in queue (creation) order and firing them in that order. Nothing sorts the expired batch by deadline. When the loop is busy (or several deadlines elapse between ticks), multiple timers are expired at the same tick and the relative order is whatever the queue order happened to be.
Confirmed pre-existing on main, not a regression: reverting timer.rs to base and rebuilding produces the identical late10, reffed output. PR #6285's single-pass partition is deliberately order-identical to the old Vec::remove(i) scan, so it neither causes nor fixes this.
Fix
This is the natural thing to fix alongside the BinaryHeap redesign already listed as a follow-up in #6084 item 3: a per-queue BinaryHeap keyed by (deadline, creation_seq) gives deadline order for free while preserving creation order as the tiebreak for same-deadline timers (which is the Node semantic that the current code does get right, and must not regress).
Part of the #6084 timer cluster.
Severity: correctness (timer ordering diverges from Node)
Found by: e2e A/B while working on #6084's timer drain (PR #6285).
What
Perry fires an expired timer batch in creation order. Node fires by deadline. So a shorter timer created later than a longer one fires second:
reffed, late10late10, reffedReproduced with a mixed burst (10 same-deadline + 5 cleared + staggered 5/10/20/30 ms). Everything else in that test matches Node byte-for-byte — same-deadline timers do fire in creation order correctly, cancelled timers never fire, and
hasRef()is right. The divergence is purely the ordering between different deadlines that are all expired at the same tick.Why
js_timer_tickcollects expired timers by walkingTIMER_QUEUEin queue (creation) order and firing them in that order. Nothing sorts the expired batch bydeadline. When the loop is busy (or several deadlines elapse between ticks), multiple timers are expired at the same tick and the relative order is whatever the queue order happened to be.Confirmed pre-existing on
main, not a regression: revertingtimer.rsto base and rebuilding produces the identicallate10, reffedoutput. PR #6285's single-pass partition is deliberately order-identical to the oldVec::remove(i)scan, so it neither causes nor fixes this.Fix
This is the natural thing to fix alongside the
BinaryHeapredesign already listed as a follow-up in #6084 item 3: a per-queueBinaryHeapkeyed by(deadline, creation_seq)gives deadline order for free while preserving creation order as the tiebreak for same-deadline timers (which is the Node semantic that the current code does get right, and must not regress).Part of the #6084 timer cluster.