Repository navigation
fix(hub): a bring-up discard reports at the level of who it strands; a sync sub-hub construction fault is not a shutdown - #5609
Conversation
…a sync sub-hub construction fault is not a shutdown Two classifications in the hub teardown path claimed facts that were false. 1. [DISPOSE-DISCARD] (event 7301) logged Error for every foreign sender's delivery parked behind the init gates of a hub torn down before bring-up finished. The sender is answered with a transient ShutdownNack, and for a SubscribeRequest every producer rides that answer out and re-asks the next activation. Nobody is stranded, yet each Orleans DirectoryFailure deactivation filed an issue (#4888, #5424, #5589). New [ReaskedOnShutdown] marker on SubscribeRequest; marked requests report at Debug with the same facts. Unmarked requests stay Error (the control). 2. SynchronizationStream refused with HubDisposingException ("is shutting down, retry") for EVERY null sub-hub, including HostedHubOutcome. ConstructionFaulted on a live host. That is how #5592 read "shutting down" on a hub whose IsShuttingDown was false. It now reads the outcome and throws an InvalidOperationException naming the root fault. Tests: DeferredDeliveryNackedOnDisposeTest (new case), SyncHubConstructionFaultIsNotAShutdownTest. Negative controls (fix lines disabled) fail both; Messaging.Hub.Test 505/505, Data.Test 545/545. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
The marker does not cover the Python producer or cross-hub RawJson deliveries.
Review effort: Lite
Findings: None
What changed in this PR
Corrects teardown log severity for re-asked subscriptions and distinguishes sync sub-hub construction faults from shutdown races.
Changes:
- Adds
[ReaskedOnShutdown]handling forSubscribeRequest. - Reports marked deferred discards at Debug while preserving transient NACKs.
- Reports construction faults as
InvalidOperationException. - Adds regression tests and updates architecture documentation.
| File | Summary |
|---|---|
test/MeshWeaver.Messaging.Hub.Test/DeferredDeliveryNackedOnDisposeTest.cs |
Tests marked and unmarked deferred-discard severity. |
test/MeshWeaver.Data.Test/SyncHubConstructionFaultIsNotAShutdownTest.cs |
Tests construction-fault and shutdown behavior. |
src/MeshWeaver.Messaging.Hub/MessageService.cs |
Applies deferred-discard classification. |
src/MeshWeaver.Messaging.Contract/ReaskedOnShutdownAttribute.cs |
Defines the request marker. |
src/MeshWeaver.Documentation/Data/Architecture/TeardownLayers.md |
Documents teardown discard levels. |
src/MeshWeaver.Documentation/Data/Architecture/RetiringAnActivation.md |
Adds stranded-work classification guidance. |
src/MeshWeaver.Documentation/Data/Architecture/HubDisposalModel.md |
Documents sync-stream refusal behavior. |
src/MeshWeaver.Data/Serialization/SynchronizationStream.cs |
Distinguishes construction faults from shutdown. |
src/MeshWeaver.Data.Contract/Messages.cs |
Marks SubscribeRequest as re-asked. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Test Results (shard 0) 1 files 1 suites 2m 58s ⏱️ Results for commit 1fea86b. |
Test Results (shard 3)429 tests 429 ✅ 1m 0s ⏱️ Results for commit 1fea86b. |
Test Results (shard 1)545 tests 545 ✅ 1m 6s ⏱️ Results for commit 1fea86b. |
Test Results (shard 4) 2 files 2 suites 6m 23s ⏱️ Results for commit 1fea86b. |
Test Results (shard 2)1 600 tests 1 600 ✅ 8m 51s ⏱️ Results for commit 1fea86b. |
Test Results (shard 5) 5 files 5 suites 14m 34s ⏱️ Results for commit 1fea86b. |
Test Results 13 files 13 suites 34m 55s ⏱️ Results for commit 1fea86b. |
Refs #4888, Refs #5424, Refs #5589, Refs #5592
Root cause
Two teardown classifications claimed facts that were false. Each false claim became an Error, and the red-log pipeline filed each Error as an issue.
1.
[DISPOSE-DISCARD](event 7301) called a re-asked subscribe "stranded work"A delivery can be parked only behind an initialization gate. Those gates close for nothing but a bring-up. So every line at this site is a teardown that ended a bring-up, which is the TeardownLayers → Handler turns carve-out. The parked delivery is answered with a transient
ShutdownNack.RetryForTheAuthoritativeAnswer(since #4866). For aSubscribeRequestevery producer rides that answer out and asks the next activation:JsonSynchronizationStream.CreateExternalClient: therejectedByRecycle→ChargeReArmBudget→Resubscribepath.Resubscribeerror arm.ResyncRefused(policy fix(routing): pod-hub claim gets a derived lifetime; PodHubNotHere is answered, not published #2745).Nobody is stranded. The line still said "Accepted work must be drained before a hub goes down" at Error. The three open incidents at this site (#4888, #5424, #5589) are all
SubscribeRequests fromcache/…senders, discarded when Orleans deactivated grains (DirectoryFailure) mid-bring-up.Fix: a new
[ReaskedOnShutdown]marker (MeshWeaver.Messaging.Contract), placed onSubscribeRequest. A marked request's discard reports at Debug with the same facts: gate, sender and teardown attribution. This follows the #4178 rule for self-addressed deliveries. Any unmarked request from another sender stays Error, because that waiter may take the NACK as final. The NACK itself is unchanged.2. A sync sub-hub construction FAULT was reported as "shutting down" (#5592)
SynchronizationStream's constructor turned every null sub-hub intoHubDisposingException("Hub X is shutting down … retry"). That includedHostedHubOutcome.ConstructionFaultedon a live host: a configuration that threw, or an OOM under heap exhaustion. #5592's line comes from the arm ofHandleInitializethat runs only whenIsShuttingDownis false. That flag reads the same monotonic flags the creation freeze reads, so ConstructionFaulted is the only null that can produce it. The issue proposed absorbing the exception during init. That would have hidden a real fault behind a teardown label.Fix: the constructor reads the outcome through
TryGetHostedHub. OnConstructionFaultedit throwsInvalidOperationException, which names the root fault and carries it as the inner exception. It is deliberately not anObjectDisposedException, so no teardown classifier reads it as "retry". Every other null is still the transientHubDisposingException.Evidence
DeferredDeliveryNackedOnDisposeTest.AReaskedRequestDiscardedByABringUpTeardown_IsTeardownNormal_NotAnError: the production initiator (NoteDirectDisposalBy("Orleans deactivating grain …")+Dispose()). It checks that the NACK is still transient and owner-recognised, that the Debug line names the gate and the attribution, and that no Error is logged. The existingDeferredRequest_IsNacked_…case (unmarked, still Error) is the control.SyncHubConstructionFaultIsNotAShutdownTest: a real fault inside the sub-hub's construction (the host servesAccessServicefrom a factory that throws while armed). The host is not shutting down, and the refusal is not anObjectDisposedExceptionand carries the injected fault. The second case is the control: the creation freeze still refuses withHubDisposingException.false &&): both new tests fail. Case 1 fails because the Debug line is missing; case 2 because "Expected value to be false … but found True". With the fix restored, both pass.dotnet build -c Release -warnaserror: 0 Warning(s) / 0 Error(s) for Messaging.Contract, Messaging.Hub, Data.Contract, Data, Documentation, Messaging.Hub.Test and Data.Test.Doc:
RetiringAnActivation→ What is NOT a retirement now carries the level table and settles the "OPEN level" paragraph.TeardownLayers→ Handler turns andHubDisposalModel(the sync-stream refusal) are updated to match.Not established
Failed to create hosted hubline.Deploy / recycle
Framework code. It takes effect with the image roll, and no address needs a manual recycle.
Pairs-with: none — nothing public is removed (one public attribute type is added).
🤖 Generated with Claude Code