Skip to content

Fix kernel console-capture ExecutionContext leak (MeshHubDisposalLeakTest flake) - #626

Closed
rbuergi wants to merge 2 commits into
mainfrom
fix/console-capture-ec-leak
Closed

rbuergi wants to merge 2 commits into
mainfrom
fix/console-capture-ec-leak

Conversation

@rbuergi

@rbuergi rbuergi commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Root cause (the flaky test was RIGHT — real product leak)

Retention path, identical in both failing runs (main 30068597014, PR #624's 30049945115):

ROOT[StrongHandle] → TimerQueue → TimerQueueTimer → ExecutionContext
  → AsyncLocalValueMap → Kernel.Hub.LoggerTextWriter → ActivityLogLogger
  → MessageHub [RunLevel=6 (disposed), kernelExec/…/_Activity/…]

CapturingTextWriter.Capture stored the script stdout/stderr pipe (writer → ActivityLogLogger → activity hub → mesh) directly in its AsyncLocal. Any long-lived timer (or pending Rx delay) created inside a capture window snapshots the ExecutionContext including that AsyncLocal map; the scope's Restorer only rewrites the current flow and can never reach frozen snapshots — so a disposed mesh stays rooted for the timer's lifetime. Intermittency: needs a long-lived timer's creation to land inside a capture window (the #620/#624 poll timers merely changed the timer population; they weren't the rooter).

Fix

The AsyncLocal now holds a mutable CaptureCell indirection; scope disposal nulls cell.Target FIRST, severing the writer graph inside every already-captured ExecutionContext. A stray snapshot keeps only the empty cell; late writes fall back to the real console instead of a disposed pipe.

Verification

  • Deterministic repro ConsoleCaptureExecutionContextLeakTest: timer created inside a capture scope, kept alive; asserts the target is collectible after scope disposal — RED pre-fix (pin reproduced in 12 ms), GREEN post-fix.
  • MeshHubDisposalLeakTest, KernelScriptMemoryLeakTest, KernelReplChainingTest (capture routing preserved — script Console output still reaches the ActivityLog) all green, non-empty trx verified.
  • CI parity build: dotnet build --no-restore -c Release -p:CIRun=true -warnaserror → 0 errors.
  • Audited all remaining AsyncLocal<> in src/ — no other instance of this leak class.

No What's New entry — internal stability fix.

🤖 Generated with Claude Code

rbuergi and others added 2 commits July 24, 2026 08:03
…hub via timer ExecutionContext

MeshHubDisposalLeakTest flaked on main run 30068597014 (shard 1) and PR #624's
run 30049945115 with the IDENTICAL ClrMD retention path in both artifacts:

  TimerQueueTimer -> ExecutionContext -> AsyncLocalValueMap
    -> MeshWeaver.Kernel.Hub.LoggerTextWriter -> ActivityLogLogger
    -> MessageHub [RunLevel=6, Address=kernelExec/Roland/_Activity/...]

Root cause: CapturingTextWriter.Capture stored the script's stdout/stderr pipe
(LoggerTextWriter -> ActivityLogLogger -> kernel activity hub -> whole mesh)
DIRECTLY in the AsyncLocal. Anything that snapshots the ExecutionContext while
a capture scope is active - a lazily-created long-lived TimerQueueTimer, a
pending Rx delay timer - freezes that AsyncLocal map into the snapshot. The
scope's Restorer only rewrites the AsyncLocal on the current flow; it can never
reach the frozen snapshots, so one such timer pinned the DISPOSED kernelExec
activity hub (created by an earlier script-running test class in the same
process, e.g. LinkedInPullActionsTest) and through it the whole disposed mesh
for the timer's lifetime. Intermittent because it needs an infrastructure
timer's first creation (or a pending delay) to land inside a capture window.

Fix (product, not test): the AsyncLocal now holds a mutable CaptureCell
indirection; disposing the capture scope nulls CaptureCell.Target FIRST, which
severs the writer graph inside every already-captured ExecutionContext - a
stray snapshot keeps only the empty ~16-byte cell. Late writes from stray
continuations now fall back to the real console instead of a disposed pipe.

Deterministic repro: ConsoleCaptureExecutionContextLeakTest creates a timer
inside a capture scope, keeps it alive, and asserts the capture target is
collectible after scope disposal - RED before this fix, GREEN after.

Verified: repro + MeshHubDisposalLeakTest + KernelScriptMemoryLeakTest +
KernelReplChainingTest (asserts Console.WriteLine output still reaches the
ActivityLog) all green; dotnet build --no-restore -c Release -p:CIRun=true
-warnaserror clean (0 errors).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wP3c7DLHutUczcBWgs4h8
Copilot AI review requested due to automatic review settings July 24, 2026 06:04

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

1 111 tests  ±0   1 008 ✅ +1   5m 10s ⏱️ -7s
   10 suites ±0     103 💤 ±0 
   10 files   ±0       0 ❌  - 1 

Results for commit 81a2d99. ± Comparison against base commit 8401ef9.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

   12 files  ±0     12 suites  ±0   4m 31s ⏱️ -20s
  965 tests ±0    961 ✅ ±0  4 💤 ±0  0 ❌ ±0 
1 027 runs  ±0  1 023 ✅ ±0  4 💤 ±0  0 ❌ ±0 

Results for commit 81a2d99. ± Comparison against base commit 8401ef9.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

934 tests  ±0   928 ✅ ±0   4m 49s ⏱️ -5s
  5 suites ±0     6 💤 ±0 
  5 files   ±0     0 ❌ ±0 

Results for commit 81a2d99. ± Comparison against base commit 8401ef9.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 946 tests  ±0   1 758 ✅ ±0   5m 16s ⏱️ -2s
   12 suites ±0     188 💤 ±0 
   12 files   ±0       0 ❌ ±0 

Results for commit 81a2d99. ± Comparison against base commit 8401ef9.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 169 tests  +1   1 168 ✅ +1   6m 8s ⏱️ - 1m 11s
   10 suites ±0       1 💤 ±0 
   10 files   ±0       0 ❌ ±0 

Results for commit 81a2d99. ± Comparison against base commit 8401ef9.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 112 tests  ±0   1 111 ✅ ±0   5m 34s ⏱️ -6s
   12 suites ±0       1 💤 ±0 
   12 files   ±0       0 ❌ ±0 

Results for commit 81a2d99. ± Comparison against base commit 8401ef9.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

   61 files  ±0     61 suites  ±0   31m 30s ⏱️ - 1m 52s
7 237 tests +1  6 934 ✅ +2  303 💤 ±0  0 ❌  - 1 
7 299 runs  +1  6 996 ✅ +2  303 💤 ±0  0 ❌  - 1 

Results for commit 81a2d99. ± Comparison against base commit 8401ef9.

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.

2 participants