Repository navigation
NodeType watchers ride SubscribeWithReEstablish — no more silent log-and-die control plane - #927
Conversation
…and-die control plane Fixes #891. InstallCompileWatcher, InstallSourcesWatcher and InstallReleaseRequestWatcher subscribed the per-NodeType own stream with a bare log-and-die OnError: one fault killed the subscription permanently and the hub kept running half-alive — compiles no longer dispatched, source tracking frozen, the Compile button silently dead — with a single warning line as the only trace. The issue's option 1 ("prove the stream cannot OnError") is disproven: SynchronizationStream.OnError terminally faults the ReplaySubject on legitimate paths (unauthorized, delivery failure, write-path failure), and the handle's content-typing boundary additionally raises per-emission MeshNodeStreamException(Deserialization) faults. All three watchers now subscribe through the canonical ActivityControlPlaneExtensions.SubscribeWithReEstablish — the reviewed post-storm primitive WatchControlPlane/WatchSubmission already use (on-fault re-establish with terminal discrimination; NOT a timer/poller watchdog) — extended with a third fault class: - Poisoned own content (MeshNodeStreamException/Deserialization): TERMINAL for re-establish purposes — the faulted stream replays the same emission, so re-establishing is a 1 Hz poison loop. Logged CRITICAL and surfaced through the new onPoisonedContent sink; the compile watcher routes it into NodeTypeCompileParkRegistry.OnCompileFailed(deterministic: true), so the type parks with a user-visible notification and diagnostics serve the cached error instead of hanging. Recovery = repair the content, recycle the hub. - Own node gone (routing NotFound): stop, as before (the 2026-06-10 storm guard). - Anything else: transient — re-establish after 1s with a fresh subscription. Replay safety on re-establish: the compile dispatch is status-guarded (Pending→Compiling atomic), the sources snapshot write is DictEquals- idempotent, and the release watcher's high-water marks live outside the re-subscribed factory so a committed trigger fails the IsPast gate. SubscribeWithReEstablish is now public (it is the watcher primitive, not an implementation detail); its doc comment names the three classes. Verified: new contract tests PoisonedContent_DoesNotResubscribe_AndReachesTheVisibleSink + IsPoisonedContent_ClassifiesOnlyDeserializationFaults (5/5 in ActivityControlPlaneResubscribeTest), and the existing watcher behavior pinned by ReleaseRequestWatcherHighWaterTest + CodeEditRecompileTest (12/12) is unchanged. Release -warnaserror builds clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Improves reliability of NodeType control-plane watchers by making their own-stream subscriptions self-healing across transient faults, while correctly treating poisoned content and own-node NotFound as terminal conditions.
Changes:
- Route NodeType compile/source/release watchers through
ActivityControlPlaneExtensions.SubscribeWithReEstablishinstead of bareSubscribe(..., onError: log). - Extend
SubscribeWithReEstablishto classify poisoned content (MeshNodeErrorCode.Deserialization) as terminal and optionally surface it viaonPoisonedContent. - Add new contract tests covering poisoned-content classification and ensuring poisoned content does not resubscribe but reaches the visible sink.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| test/MeshWeaver.Hosting.Test/ActivityControlPlaneResubscribeTest.cs | Adds tests for poisoned-content terminal behavior and classification. |
| src/MeshWeaver.Mesh.Contract/ActivityControlPlaneExtensions.cs | Makes SubscribeWithReEstablish public, documents fault classes, and adds poisoned-content handling. |
| src/MeshWeaver.Graph/Configuration/NodeTypeCompilationHelpers.cs | Switches NodeType watchers to use SubscribeWithReEstablish, including parking on poisoned content for the compile watcher. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Test Results (shard 4)1 207 tests 1 202 ✅ 5m 26s ⏱️ Results for commit de71664. ♻️ This comment has been updated with latest results. |
Test Results (shard 2)1 777 tests 1 775 ✅ 6m 6s ⏱️ Results for commit de71664. ♻️ This comment has been updated with latest results. |
Test Results (shard 0)673 tests 673 ✅ 4m 58s ⏱️ Results for commit de71664. ♻️ This comment has been updated with latest results. |
Test Results (shard 5)1 676 tests 1 485 ✅ 5m 33s ⏱️ Results for commit de71664. ♻️ This comment has been updated with latest results. |
Test Results (shard 1)1 185 tests 1 185 ✅ 7m 43s ⏱️ Results for commit de71664. ♻️ This comment has been updated with latest results. |
Test Results (shard 3) 11 files 11 suites 5m 30s ⏱️ Results for commit de71664. ♻️ This comment has been updated with latest results. |
Test Results 65 files 65 suites 35m 19s ⏱️ Results for commit de71664. ♻️ This comment has been updated with latest results. |
Fixes #891.
The hazard
The three NodeType control-plane watchers (
InstallCompileWatcher,InstallSourcesWatcher,InstallReleaseRequestWatcher) subscribed the own stream withSubscribe(onNext, ex => log). One fault terminated the subscription permanently and silently: compiles stopped dispatching, source tracking froze (IsDirtydead, parked-type auto-retry dead), and the Compile button did nothing forever — a hub running half-alive, the wedges-to-zero violation.Why option 1 (prove no OnError) is off the table
SynchronizationStream.OnErrorterminally faults the ReplaySubject on legitimate paths (unauthorized, delivery failure, write-path failure), andMeshNodeStreamHandle's content-typing boundary raises per-emissionMeshNodeStreamException(Deserialization)faults by design (the GUI renders a typed error card off them).The fix
All three watchers now subscribe through the canonical
SubscribeWithReEstablish— the reviewed post-storm primitiveWatchControlPlane/WatchSubmissionalready use (fault-driven re-establish with terminal discrimination; not a timer/poller watchdog — it reacts only to an actualOnErrorand stops on terminal classes). Extended with a third fault class:Deserialization)onPoisonedContent— the compile watcher parks the type viaOnCompileFailed(deterministic: true), so the failure is user-visible (park notification, diagnostics serve the cached error) instead of one warning lineReplay safety on re-establish: compile dispatch is status-guarded (Pending→Compiling atomic), the sources snapshot write is idempotent (
DictEquals), and the release watcher's high-water marks live outside the re-subscribed factory so committed triggers fail theIsPastgate — no double compile.SubscribeWithReEstablishbecomes public (it is the watcher primitive), with the fault classes documented.Verification
PoisonedContent_DoesNotResubscribe_AndReachesTheVisibleSink,IsPoisonedContent_ClassifiesOnlyDeserializationFaults— 5/5 inActivityControlPlaneResubscribeTest.ReleaseRequestWatcherHighWaterTest+CodeEditRecompileTest— 12/12 unchanged.-warnaserrorbuilds clean.No What's New entry — latent-hazard resilience fix, no observed user-facing behavior change on healthy paths.
🤖 Generated with Claude Code