On a roll the old pod keeps serving its open Blazor circuits while it drains. The mesh's teardown
then drains the I/O pool (IoPoolRegistry.DrainAll), and every read still in flight ends with a
cooperative cancellation: TaskCanceledException: A task was canceled. The circuit is still open, so
the cancellation reaches views that are not yet disposed. BlazorView.SurfaceError used to treat
only the view's own disposal as teardown. It therefore logged the process's teardown at Error and
showed the error modal.
Measured once (MeshWeaver#7020): during a roll of an AKS instance, three lines
fail: ThreadChatView … Resolving the composer's context arrived within the same millisecond, from
two ThreadChatViews of one circuit (areas Composer and Thread/1). They came from the old pod, while the
roll's "Verify one generation" phase was running.
| The fault is… | while… | SurfaceError does |
|---|---|---|
| anything | the view is disposed, or it is an ObjectDisposedException |
Debug, no modal (unchanged) |
a cooperative cancellation (CancellationClassifier.IsCooperativeCancellation) |
the portal hub is shutting down or the host has begun stopping (hub.IsLeaving()) |
Debug, no modal |
| a cooperative cancellation | the host keeps running | Error, the modal: a binding ended for no stated reason |
a cancellation that expresses a timeout (a TimeoutException inside it) |
anything | Error, the modal: an availability failure |
a SyncStreamOwnerRefusedException at any depth (a stream that gave up re-subscribing to a ShuttingDown owner) |
the host is leaving | Debug, no modal |
a SyncStreamOwnerRefusedException |
the host keeps running | Error, the modal: an owner that did not come back |
The classifier is BlazorView.IsHostTeardownCancellation(error, hostLeaving). The probe is asked
only for a cooperative cancellation. A hub whose scope is already disposed counts as leaving.
The same teardown, answered by routing (MeshWeaver#7575)
Not every read in flight is an I/O leaf. A read that needs a re-subscribe while the process is
stopping is refused by that process's own router: Host is shutting down, cannot route to …. The sync
stream rides that out on its bounded budget (see core's Doc/Architecture/RidingOutAShuttingDownAddress),
and then ENDS with SyncStreamOwnerRefusedException so that its readers can re-read. On a terminating
replica no re-read can be answered: the circuit dies with the pod, and its reconnect lands on a live
replica. Measured once: in one second of a roll, a terminating pod logged 30 or more such faults across
every view of its open circuits (CollaborativeMarkdownView, MarkdownEditorView, ThreadChatView,
ButtonView, ContainerView and others), each at Error and each in the modal.
The classifier is BlazorView.IsHostTeardownRefusal(error, hostLeaving), and it uses the same
probe. The same refusal on a host that keeps running stays fail-visible, because there it means an
owner that never came back.
Every view that routes its onError through SurfaceError gets this arm, directly or through
SurfaceReadError or the data-binding fault path. ThreadChatView's composer context chip is one of them.
Regression tests: HostTeardownCancellationTest.AReadCancelledByTheTeardownOfAStoppingHost_RaisesNoErrorModal
and its control, AReadCancelledWhileTheHostKeepsRunning_StillRaisesTheErrorModal. Both bind a view
to a leaf of a real IoPool and end it with a real Drain(). The control proves the silence is the
probe-gated arm, not a fault that never arrived. OnlyACooperativeCancellationOnALeavingHost_IsHostTeardown
pins the predicate's boundary. For the routing refusal:
AReadRefusedByTheRouterOfAStoppingHost_RaisesNoErrorModal, its control
AReadRefusedWhileTheHostKeepsRunning_StillRaisesTheErrorModal, and
OnlyAnOwnerRefusalOnALeavingHost_IsHostTeardownRefusal.