Skip to content

Defer the probe's query legs: a subscribe-time throw was killing the test host - #717

Merged
rbuergi merged 1 commit into
mainfrom
fix/probe-subscribe-throw-crashes-host
Jul 29, 2026
Merged

rbuergi merged 1 commit into
mainfrom
fix/probe-subscribe-throw-crashes-host

Conversation

@rbuergi

@rbuergi rbuergi commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Root cause of the CI exit=2 with an all-green trx shard failures — pinned from the failing run's own logs (#716's run), not inferred:

[FATAL ERROR] System.ObjectDisposedException
  at AutofacServiceProvider.GetService                ← disposed scope
  at MeshService.CaptureContext      (MeshService.cs:62)
  at MeshService.Query[T]            (MeshService.cs:257)
  at MeshNodeCompilationService.DirectSourceProbe b__2 (:618)
  at CombineLatest.Run ← SubscribeRaw ← DelaySubscription ← ThreadPool

Mechanism

The probe's per-leg mesh.Query(...) lambda is evaluated inside CombineLatest's subscription, and the probe's subscription is scheduled (DelaySubscription → a ThreadPool tick). When the mesh is disposed between scheduling and firing, CaptureContext throws synchronously on the dead Autofac scope — and Rx routes a synchronous throw during a Producer's subscribe to the subscribe caller, not to OnError, so neither the leg's .Catch nor the probe's outer .Catch ever sees it. The caller is the scheduler → unhandled on the ThreadPool → xUnit v3's AppDomain handler reports [FATAL ERROR], waits for the run to finish (finishEvent.Wait() — which is why the summary prints all green, Errors: 0), then Environment.Exit(2). TeardownStragglerCapturer never sees it: xUnit's handler subscribed first and exits the process before later handlers run.

This is why the exit=2 transients began exactly when #690 made this probe run on every compile (previously a delay-subscribed fallback that almost never fired): #708 attempt 1 (AI.Test + Acme.Test, both compile-heavy), attempt 2 green on the identical commit (won the timer race), #716 (base includes #690, its own diff is fixture-only).

Fix

Wrap each leg in Observable.Defer. Defer's factory exceptions are forwarded to OnError, so the throw lands in the leg's existing .Catch and the leg degrades to empty — the exact behaviour the probe already intends for a faulted leg ("NEVER win the race with an EMPTY set… stay silent").

No deterministic in-repo repro: it needs the container to die between the probe's schedule and its tick — a window #715's construction drain also narrows. The CI FATAL stack above is the repro; the fix is the Rx-contract correction that makes the window harmless.

Verification

No What's New entry: internal reliability fix, no user-visible change.

Related: #690 (made the window hot), #708/#716 (the runs it redded), #613 (the caught-straggler class is adjacent but distinct — this one was the unhandled escape).

🤖 Generated with Claude Code

…test host

Root cause of the CI "exit=2 with an all-green trx" shard failures — pinned
from the failing run's own logs, not inferred:

    [FATAL ERROR] System.ObjectDisposedException
      at AutofacServiceProvider.GetService                ← disposed scope
      at MeshService.CaptureContext      (MeshService.cs:62)
      at MeshService.Query[T]            (MeshService.cs:257)
      at MeshNodeCompilationService.DirectSourceProbe b__2 (:618)
      at CombineLatest.Run ← SubscribeRaw ← DelaySubscription ← ThreadPool

The probe's per-leg `mesh.Query(...)` lambda is evaluated inside CombineLatest's
SUBSCRIPTION, and the probe's subscription is SCHEDULED (DelaySubscription → a
ThreadPool tick). When the mesh is disposed between scheduling and firing,
CaptureContext throws synchronously on the dead Autofac scope — and Rx routes a
synchronous throw during a Producer's subscribe to the SUBSCRIBE CALLER, not to
OnError, so neither the leg's .Catch nor the probe's outer .Catch ever sees it.
The caller is the scheduler → unhandled on the ThreadPool → xUnit v3's
AppDomain handler reports "[FATAL ERROR]", waits for the run to finish
(finishEvent.Wait() — which is why the summary prints all green, Errors: 0),
then Environment.Exit(2). Our TeardownStragglerCapturer never sees it: xUnit's
handler subscribed first and exits the process before later handlers run.

This is why the exit=2 transients began exactly when #690 made this probe run
on every compile (before, it was a delay-subscribed fallback that almost never
fired): #708 attempt 1 (AI.Test + Acme.Test, both compile-heavy), attempt 2
green on the identical commit (won the timer race), #716 (base includes #690).

Fix: wrap each leg in Observable.Defer. Defer's FACTORY exceptions ARE
forwarded to OnError, so the throw lands in the leg's existing .Catch and the
leg degrades to empty — the exact behaviour the probe already intends for a
faulted leg ("NEVER win the race with an EMPTY set… stay silent").

No deterministic in-repo repro: it needs the mesh container to die between the
probe's schedule and its tick, a window the test base's drain now also narrows
(#715). The CI FATAL stack above is the repro; the fix is the Rx-contract
correction that makes the window harmless.

Verified: CompileLeafStabilityTest 2/2 + CompileSourceSnapshotWedgeTest 1/1
(the stalled-snapshot path), Graph builds clean under -c Release -warnaserror.

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

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.

Pull request overview

This PR fixes a teardown-time reliability issue in the Graph compilation source probe where a scheduled subscription could run after the mesh/DI scope is disposed, causing a synchronous ObjectDisposedException to escape Rx error handling and crash the xUnit host (CI exit=2 despite all-green TRX).

Changes:

  • Wrap each per-query “leg” inside DirectSourceProbe with Observable.Defer so any synchronous exceptions thrown during subscription are surfaced via OnError and handled by the existing per-leg .Catch(...) fallback.
  • Add an in-code explanation of the Rx contract nuance (subscribe-time throws escaping to the subscribe caller) and why per-leg deferral is required even though the outer probe is already delayed.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

1 129 tests   1 026 ✅  5m 10s ⏱️
   10 suites    103 💤
   10 files        0 ❌

Results for commit a1c2433.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

2 100 tests  ±0   1 912 ✅ ±0   4m 51s ⏱️ -1s
   12 suites ±0     188 💤 ±0 
   12 files   ±0       0 ❌ ±0 

Results for commit a1c2433. ± Comparison against base commit dd1ed4c.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

959 tests   953 ✅  4m 44s ⏱️
  5 suites    6 💤
  5 files      0 ❌

Results for commit a1c2433.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

   12 files  ±0     12 suites  ±0   3m 52s ⏱️ -9s
  977 tests ±0    973 ✅ ±0  4 💤 ±0  0 ❌ ±0 
1 041 runs  ±0  1 037 ✅ ±0  4 💤 ±0  0 ❌ ±0 

Results for commit a1c2433. ± Comparison against base commit dd1ed4c.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 164 tests  ±0   1 163 ✅ ±0   5m 15s ⏱️ -37s
   12 suites ±0       1 💤 ±0 
   12 files   ±0       0 ❌ ±0 

Results for commit a1c2433. ± Comparison against base commit dd1ed4c.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 202 tests  +421   1 201 ✅ +420   6m 55s ⏱️ + 4m 53s
   10 suites +  1       1 💤 +  1 
   10 files   +  1       0 ❌ ±  0 

Results for commit a1c2433. ± Comparison against base commit dd1ed4c.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

   61 files  +  1     61 suites  +1   30m 49s ⏱️ + 5m 25s
7 531 tests +421  7 228 ✅ +420  303 💤 +1  0 ❌ ±0 
7 595 runs  +421  7 292 ✅ +420  303 💤 +1  0 ❌ ±0 

Results for commit a1c2433. ± Comparison against base commit dd1ed4c.

@rbuergi
rbuergi merged commit d875505 into main Jul 29, 2026
16 checks passed
@rbuergi
rbuergi deleted the fix/probe-subscribe-throw-crashes-host branch August 5, 2026 10:45
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