Conversation
…ed timer Node re-initializes a Timeout's async resource when refresh() is called on a timer whose callback has already run (lib/internal/timers.js insertGuarded), so the reactivated timer observes the AsyncLocalStorage context of the refresh() caller. Bun kept the creation-time AsyncContextFrame forever. do_refresh() now unwraps the stored AsyncContextFrame and re-snapshots the currently active async context when the timer is already destroyed. Pending timers and refresh() from inside the timer's own callback are unchanged, which also matches Node.
|
Updated 1:15 PM PT - Jun 29th, 2026
❌ @robobun, your commit 363b8ac has 2 failures in
🧪 To try this PR locally: bunx bun-pr 33092That installs a local version of the PR into your bun-33092 --bun |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
|
Warning Review limit reached
Next review available in: 7 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (4)
WalkthroughAdds ChangesAsyncLocalStorage context recapture for Timeout#refresh
Docs formatting fixes
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/js/node/async_hooks/AsyncLocalStorage.test.ts`:
- Around line 129-166: The AsyncLocalStorage timer refresh test covers pending,
after-fired, and unbound refresh paths, but misses the in-callback `refresh()`
case in `AsyncLocalStorage.test.ts` around the `setTimeout().refresh()`
scenario. Extend the existing `callback`/`onFire` flow so one invocation calls
`t.refresh()` before returning, then assert the subsequent rearmed execution
preserves the current store value rather than rebinding or clearing it. Keep the
new coverage aligned with the existing `s.run(...)`, `t.refresh()`, and `seen`
assertions so the behavior change is locked down in the same test.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 706d6e01-8918-47db-a3c1-fae7bccb3392
📒 Files selected for processing (7)
docs/guides/util/base64.mdxdocs/runtime/web-apis.mdxsrc/jsc/JSValue.rssrc/jsc/bindings/AsyncContextFrame.cppsrc/runtime/timer/timer_object_internals.rstest/js/node/async_hooks/AsyncLocalStorage.test.tstest/js/node/async_hooks/async-context/async-context-timers-refresh.js
A refresh() issued while the callback is still running must keep the context the timer is already bound to (the timeout is not destroyed yet), in Bun and in Node. Exercises the in_callback branch of get_destroyed() that do_refresh relies on.
`timeout._onTimeout` is user-assignable. Wrapping a non-callable in an AsyncContextFrame made it truthy, so fire() no longer treated falsy values as cleared, and the not-a-function error path in Bun__JSTimeout__call returned before restoring the async context, leaving the refresh() caller's AsyncLocalStorage context installed globally. Only re-wrap values the timer can invoke (a callable or a Bun.sleep promise), and restore the async context before that early return.
|
CI status for 363b8ac (build 66882): every failing lane is unrelated to this change.
Every lane that ran the suites covering this change is green, including the debian-13 x64 ASAN lane; |
|
Stale PR review: keep open, rework. The behaviour is wanted, and main still differs from Node. On 1.4.3-canary (367d939), a timer created in The diff is not the shape to merge:
The |
Problem
timeout.refresh()on a timer whose callback has already run reactivates the timer, but in Bun the callback kept firing in the AsyncLocalStorage context the timer was created in. Node re-binds it to the context of therefresh()caller.Against node v26.3.0 (same result with
--no-async-context-frame):BBBundefinedBBundefinedrefresh()inside the timer's own callbackThis is the idle/keep-alive timeout idiom (
refresh()from per-request contexts onto a connection-scoped timer), so tracing code instrumented for node attributes the timeout to the wrong context under Bun.Cause
Node's
Timeout.prototype.refresh()goes throughinsertGuarded, which callsinitAsyncResourceonly when the timeout is destroyed (its callback already ran) or has no async id.initAsyncResourcestoresAsyncContextFrame.current()on the resource, so reactivating a fired timer snapshots the refresh-time context. A still-pending timer is never re-initialized.In Bun the creation-time context is baked into the
AsyncContextFramewrapper stored as the Timeout's callback, anddo_refresh()never touched it.Fix
When
do_refresh()runs on a timer that is already destroyed (fired and not cleared), unwrap the storedAsyncContextFrameand re-snapshot whatever async context is active at therefresh()call via a newAsyncContextFrame__recaptureAsyncContextIfNeededhelper. Pending timers andrefresh()from inside the timer's own callback are unchanged, matching Node in both cases.Verification
test/js/node/async_hooks/async-context/async-context-timers-refresh.jsis picked up byAsyncLocalStorage-tracking.test.ts, which runs every fixture under bothbunandnodeand requires exit 0 from both. It fails on current Bun (["creator","creator","creator","creator"]instead of["creator","refresher",null,"again"]) and passes with the fix.bun bd test test/js/node/async_hooks/passes (113 tests, including the node cross-run).