Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions src/runtime/timer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,17 @@ impl All {
// SAFETY: caller guarantees `timer` is a valid live EventLoopTimer.
let tag = unsafe { (*timer).tag };
debug_assert!(tag != EventLoopTimerTag::WTFTimer, "use wtf_arm");

// Bump the global epoch into the per-timer flags so equal-deadline JS
// timers (setTimeout/setInterval/AbortSignal.timeout) fire in insertion
// order. Before heap insert: `EventLoopTimer::less` reads epoch as tiebreak.
// SAFETY: `timer` is live (caller contract).
if let Some(flags) = unsafe { js_timer_flags_ptr(timer) } {
self.epoch = self.epoch.wrapping_add(1) & ((1u32 << 25) - 1);
// SAFETY: `flags` points into the live container recovered above.
unsafe { (*flags.as_ptr()).set_epoch(self.epoch) };
}

if self.fake_timers.is_active() && tag.allow_fake_timers() {
// SAFETY: see fn contract
unsafe {
Expand Down Expand Up @@ -829,17 +840,8 @@ impl All {
timer_ref.next.sec = time.sec;
timer_ref.next.nsec = time.nsec;

// Bump the global epoch and write it back
// into the per-timer flags so equal-deadline JS timers fire in
// refresh order.
// SAFETY: `timer` is live (caller contract); `timer_ref`'s last use
// is above so the raw `(*timer).tag` read inside is SB-clean.
if let Some(flags) = unsafe { js_timer_flags_ptr(timer) } {
self.epoch = self.epoch.wrapping_add(1) & ((1u32 << 25) - 1);
// SAFETY: `flags` points into the live container recovered above.
unsafe { (*flags.as_ptr()).set_epoch(self.epoch) };
}

// `insert` bumps the global epoch and writes it into the per-timer
// flags so equal-deadline JS timers fire in refresh order.
self.insert(timer);
}

Expand Down
28 changes: 28 additions & 0 deletions test/js/web/abort/abort.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,32 @@
expect(fmt(ac.reason)).toEqual(fmt(new DOMException("The operation timed out.", "TimeoutError")));
expect(ac.reason.code).toBe(23);
});

// https://wpt.fyi/results/dom/abort/timeout.any.html "AbortSignal timeouts fire in order"
test("AbortSignal.timeout with equal deadlines fire in creation order", async () => {
const src = `
const order = [];
const done = Promise.withResolvers();
let remaining = 7;
const tick = v => { order.push(v); if (--remaining === 0) done.resolve(); };
for (let i = 0; i < 6; i++) {
const s = AbortSignal.timeout(5);
s.onabort = () => tick(i);
}
// setTimeout with the same delay is a reference: it already fires in
// creation order, and these signals should sort alongside it.
setTimeout(() => tick("t"), 5);
await done.promise;
console.log(JSON.stringify(order));
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", src],
env: bunEnv,
stderr: "pipe",
});

Check warning on line 112 in test/js/web/abort/abort.test.ts

View check run for this annotation

Claude / Claude Code Review

Test asserts stderr is exactly empty (flaky under ASAN/debug)

This `expect(stderr).toBe("")` will flake on ASAN/debug CI lanes, which emit benign diagnostics to stderr — REVIEW.md explicitly forbids exact-empty stderr assertions for that reason. Drop this line (the `JSON.parse(stdout)` and `exitCode` assertions already prove correctness) or assert a combined `{ stdout, stderr, exitCode }` object via `normalizeBunSnapshot`.
Comment thread
robobun marked this conversation as resolved.
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(JSON.parse(stdout.trim())).toEqual([0, 1, 2, 3, 4, 5, "t"]);
expect(exitCode).toBe(0);
});
});
Loading