Skip to content

fix(runtime-events): plug waitForRuntimeEvent handle leak (CONNECTION_ENDED race) - #1176

Merged
namastex888 merged 1 commit into
devfrom
fix/runtime-events-handle-leak
Apr 18, 2026
Merged

namastex888 merged 1 commit into
devfrom
fix/runtime-events-handle-leak

Conversation

@namastex888

Copy link
Copy Markdown
Contributor

Summary

Plugs the leak that produced the rare CONNECTION_ENDED flake on PR #1171's push-CI (run 24589853586). HIGH-confidence tracer diagnosis pinned to waitForRuntimeEvent in src/lib/runtime-events.ts.

Root cause (tracer, 1-2 sentences)

waitForRuntimeEvent's IIFE awaits followRuntimeEvents, which internally drains before returning. When the awaited event matches during that initial drain, finish() resolves the outer Promise with handle still null; the IIFE then assigns the handle (active 250ms poller) but settled === true, so handle.stop() never runs. The leaked poller calls listRuntimeEvents → getConnection() for the rest of the test process and races resetConnection() calls from later test files, occasionally landing a CONNECTION_ENDED rejection on a real test's mailbox.send().

Changes (one file: src/lib/runtime-events.ts)

  1. waitForRuntimeEvent: handle the late-handle case (stop the just-arrived handle when settled was already flipped during drain), and try/catch the IIFE so followRuntimeEvents exceptions reject the outer Promise instead of becoming unhandled rejections.
  2. followRuntimeEvents.stop(): wrap await drainChain and await listener.unlisten() in try/catch so stop() never throws CONNECTION_ENDED. Mirrors the swallow pattern at src/lib/audit.ts:170-174.

Relation to upstream 74aaa022 fix(db): null sqlClient before ending

Related but incomplete. 74aaa022 narrowed the window where resetConnection() would expose torn-down clients via getConnection(), but it does not cover sql references already captured into closures by leaked pollers. This fix removes the leak source, so even captured-then-stale references stop being created.

Regression coverage

Regression test for the rare race is staged follow-up — would require forced-race fixtures (controllable resetConnection() timing relative to an in-flight followRuntimeEvents poll). Out of scope for this minimal correctness fix.

Test plan

  • bun test src/lib/runtime-events.test.ts — 7 pass / 0 fail
  • bun test src/lib/runtime-events.test.ts src/term-commands/log.test.ts src/lib/mailbox.test.ts — 21 pass / 0 fail
  • bun run check — 2581 pass / 0 fail
  • Full suite x5: 4 clean, 1 unrelated SpawnTargetPicker > Esc flake (confirmed pre-existing on baseline origin/dev: 3/8 fails on the same TUI test without this patch)
  • Scope confirmed: 1 file touched (src/lib/runtime-events.ts)

🤖 Generated with Claude Code

…_ENDED race)

Root cause: waitForRuntimeEvent's IIFE awaits followRuntimeEvents, which
internally drains before returning. If the awaited event matches during that
initial drain, finish() resolves the outer Promise with handle still null;
the IIFE then assigns the handle (active 250ms poller) but settled===true,
so handle.stop() never runs. The leaked poller calls listRuntimeEvents →
getConnection() forever, racing against subsequent test files'
resetConnection() and producing CONNECTION_ENDED rejections that
occasionally land on a real test's mailbox.send().

Two layered changes in src/lib/runtime-events.ts:
1. waitForRuntimeEvent: handle the late-handle case (stop the just-arrived
   handle when settled was already flipped during drain), and try/catch the
   IIFE so followRuntimeEvents exceptions reject the Promise instead of
   becoming unhandled rejections.
2. followRuntimeEvents stop(): wrap drainChain await and listener.unlisten()
   in try/catch so stop() never throws CONNECTION_ENDED. Mirrors the swallow
   pattern already used in src/lib/audit.ts:170-174.

Related to upstream 74aaa02 fix(db): null sqlClient before ending — that
fix narrowed the race window but did not cover sql references already
captured into closures by leaked pollers.

Regression test for the rare race is staged follow-up — would require
forced-race fixtures (controllable resetConnection timing relative to a
mid-flight poll). Out of scope for this minimal correctness fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: de865a62-e84a-4b27-9cbf-f436a5a114b2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/runtime-events-handle-leak

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enhances the robustness of runtime event handling by adding error boundaries around cleanup tasks and event polling, ensuring that connection teardown errors are gracefully swallowed. Additionally, waitForRuntimeEvent now correctly handles promise rejections and manages late-arriving handles to prevent resource leaks. I have no feedback to provide.

@namastex888
namastex888 merged commit 477d999 into dev Apr 18, 2026
6 checks passed
@automagik-genie
automagik-genie deleted the fix/runtime-events-handle-leak branch September 25, 2026 04:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant