Repository navigation
MeshWeaver.FutuRe.Test SIGSEGVs after all tests pass: SourcesWatcher keeps requesting against a hub disposed 2s earlier, and reddens a required Plugins gate #3026
Description
MeshWeaver.FutuRe.Test intermittently crashes its test host with SIGSEGV (exit 139) after every test has passed. It reddens Build + test the portal hosts — a required context on MeshWeaver.Plugins — so a green PR becomes an un-mergeable one for a reason nothing in the diff can reach. Observed on Plugins PR #1122 (run 33570183207, shard 3) while shards 0/1/2 passed and a sibling PR's run had all four green, so it is intermittent rather than deterministic.
The CI harness's own annotation states the class exactly, and I am quoting it rather than re-deriving it:
THE TEST HOST EXITED 1 WITH NO FAILING TEST. Every test passed and the host STILL exited non-zero, so nothing failed ON a test thread and there is no failing assertion to find. An exception escaped on a non-test thread and reached
AppDomain.UnhandledException… xUnit v3 does NOT hookTaskScheduler.UnobservedTaskException, so this can only be a raw throw on a timer / ThreadPool / dedicated thread — typically a DI resolve inside a single-argumentSubscribe(onNext)body, which Rx RETHROWS rather than routing to onError. Fix it by GATING the callback on the hub's teardown state, not by catching the exception; do not re-run.
The measured mechanism: a watcher that keeps running after dispose
From the shard's _meshweaver-test-trace.log (pid 51286), one fixture's teardown:
23:30:20.819 DISPOSE_CLIENTS_DONE elapsed=13ms
23:30:20.825 DISPOSE_INVOKED elapsed=19ms
23:30:22.877 [FAULT] [Warning] [MeshWeaver.Graph.CompileWatcher] SourcesWatcher: the @@-include
closure for FutuRe/LocalAnalysis could not be established …
MeshWeaver.Graph.Configuration.SourceIncludeUnavailableException
---> System.ObjectDisposedException: Hub FutuRe/LocalAnalysis was disposed before
the response arrived (request type GetDataRequest,
target FutuRe/GroupAnalysis/Source/ExternalDependencies).
23:30:22.894 DISPOSE_DONE elapsed=2088ms teardown clean — all pooled I/O joined, async dispose queue drained
So SourcesWatcher is still issuing GetDataRequests two seconds after DISPOSE_INVOKED, against a hub that is already gone. It happens 22 times in this one shard, every one of them for FutuRe/LocalAnalysis, spanning 23:30:22.877 → 23:31:47.274 — i.e. it continues for a minute and a half, across subsequent fixtures.
Note what the teardown assertion says while this is happening: teardown clean — all pooled I/O joined, async dispose queue drained. The straggler is not in the pooled-I/O set the fixture knows how to join, which is why the existing teardown gate does not catch it.
The 22 logged instances are caught (they surface as [FAULT] [Warning]). The crash is the one that is not — same window, same watcher, a path where the throw reaches a raw thread instead of the log. A core dump for the crashing process is in the run's artifact teardown-stragglers-33570183207-1-shard3 (MeshWeaver.Futu-51286.dmp, 1.3 GB) if you want the stack.
Where it lives
Core, not Plugins: MeshWeaver.Graph.CompileWatcher / SourcesWatcher, and SourceIncludeUnavailableException is src/MeshWeaver.Compiler.Pipeline (SourceFingerprintIncludeReader.cs, NodeTypeCompilationHelpers.cs). The suite is merely the observation site — every frame in the failure is core code.
What the fix is not
Per the standing directive, exit 139 is use-after-dispose, never a flake to retry, so please do not close this by re-running or by wrapping the throw in a catch. Catching would convert a crash into 22 more warnings and leave the watcher doing work for a disposed hub. The watcher needs to observe the hub's teardown state and stop — and the fixture's teardown clean verdict should not be able to read clean while a watcher of one of its hubs is still issuing requests.
Possibly the same subject
MeshWeaver.Plugins#1141 (now closed) recorded a one-sample 50s timeout in this same suite where the area resolved to a RedirectControl pointing at itself. I disproved its stated cause (the type-registration warnings appear on green runs too — 114 of them across 28 types on a 2.9s passing run). A compile watcher that cannot establish a source-include closure is a plausible route to a view that never renders its control, so the two may share a root. Not asserted — flagging it so whoever fixes this checks.
Activity
Merge pull request #3072 from Systemorph/fix/3026-sources-watcher-obs…
rbuergi commentedon Sep 2, 2026
Fixed by #3072 (merged today). Hub-owned watchers (sources/compile/release/adopted-stamp) stop at the first instant of teardown via the new IMessageHub.ShuttingDown signal and SubscribeHubWatcher; no request is issued against a disposing hub, so the raw-thread throw that killed the FutuRe test host has no path.
🤖 Generated with Claude Code
rbuergi commentedon Sep 3, 2026
Recurred twice against trees that contain #3072 — and the dump says the remaining fault is not a watcher
Not asking to reopen; recording the measurement so the fix is not read as complete.
Base rate,
Plugin Catalog CI, 1,197 runs over 2026-08-29 → 2026-09-03 (every failed run's every failed job's annotations read — 7,362 rows — plus all 92 cancelledmainruns, so a crash that preceded a cancellation could not hide):5 occurrences, all
MeshWeaver.FutuRe.Test, one onmain(run33575831719). No other suite shows the signature. Onset 2026-09-01T23:15; nothing matching in the preceding ~2.5 days.Two of the five ran against core commits that already contain #3072 (merge
17ee6d9fa, 2026-09-02T08:43Z):17ee6d9fa7e9858ece7f1d6993 sightings in the ~9 h before the fix, 2 in the ~32 h after: the rate dropped, the hole did not close.
What the later dump shows
I pulled the core dump out of run
33778900486's own artifact (MeshWeaver.Futu-49849.dmp):A hardware page fault at address 0 on a dedicated background-GC thread,
RIPinside a file-backedlibcoreclr.so, zero managed frames on the faulting thread. Disposal in that process was clean and the trace record is provably complete (bothFAULT-BUDGETlines belong to a different pid): everyDISPOSE_DONEreadsteardown clean — all pooled I/O joined, async dispose queue drained,leakedIoLeaves=0, zeroDISPOSE_QUIESCE_LEAK, zeroDISPOSE_DIRTY_TEARDOWN.That is sighting #9 of the zeroed-MethodTable family in
Doc/Architecture/DebuggingNativeCrashes— a fourth distinctgc_heapframe, same dereference. Written up in #3241.So the reading that fits both halves is the one that page already states: the watcher work was part of the workload that provokes a CoreCLR GC fault, not the fault itself. Removing it lowered the exposure; what remains is the family, and it is still live on
main.The reporting has been fixed too — Systemorph/MeshWeaver.Plugins#1289 — because the shard's classifier had been calling this SIGSEGV an escaped exception and printing a static remedy naming a DI resolve in a
Subscribe(onNext), which is what sent #3229 at a teardown gate.