Repository navigation
Second write racing a retype's hub recycle is refused as Unknown instead of retry-this #6034
Description
Moved from the fleet's private triage inbox (MeshWeaver.Feedback#18, filed 2026-09-22). Core owns it: src/MeshWeaver.Mesh.Contract/MeshNodeStreamExtensions.cs (re-enqueue set) and NodeTypeRebindWatcher are core.
Retyping a node recycles its per-node hub (NodeTypeRebindWatcher, MeshWeaver issue 1104). A SECOND write issued straight after the first therefore arrives while that hub is mid-disposal and is REFUSED: MeshNodeStreamException, MeshNode Unknown at TestData/occ04de1b70: Hub TestData/occ04de1b70 is shutting down (RunLevel=DisposeHostedHubs, activation 03B0177E) - cannot process RawJson; the address may reactivate (recycle / restart). Rejecting now. The upsert surfaces it as success=False reason=Unknown.
Why it matters: a full-node update naming a type that DOES resolve is the ONLY repair route for a mistyped node - patch refuses nodeType outright - and Doc/Architecture/DanglingNodeTypes commits to keeping it open. If the retype that STRANDED a node and the retype that REPAIRS it can land close together, the repair is refused for a reason that has nothing to do with the rule, and the caller sees a generic Unknown rather than retry-this.
How I got here: writing a control for MeshWeaver issue 5008 in test/MeshWeaver.Graph.Test/NodeTypePathOccupancyTest.cs. The test did an escape-hatch upsert that retyped a node onto an occupied path, then immediately upserted it back to a type that resolves. It PASSED when the class ran alone and FAILED in the full 1951-test project run, at 2026-09-22 13:52:34 local. I removed the racing step rather than widen a wait, and left the reasoning in a comment at that site.
What I did NOT establish: (1) no production evidence at all - this is a local test observation, and I did not look for the shape in any portal log; (2) I did not establish whether the refusal is genuinely transient, i.e. whether a plain retry lands - the NACK text says the address may reactivate, but I did not test it; (3) I did not establish whether an ordinary caller (the MCP update tool, the editor) can realistically issue two retypes inside the disposal window, or whether only a test can; (4) I did not check whether any retry/re-enqueue already covers this - MeshNodeStreamExtensions names some NACKs as provably-safe and re-enqueues them, and a shutdown NACK may or may not be in that set. That last one decides whether this is a defect or working as intended, and it is the first thing to check.
Pre-existing, not introduced by the issue-5008 change: the rebind recycle on retype predates it and the change adds no write.
Where: (no main node — submitted from the user's Feedback area, path rbuergi/Feedback/20260922-1155-repair-write-races-the-type-rebind-recycle)
Context: Reported while writing a control test for MeshWeaver issue 5008 in test/MeshWeaver.Graph.Test/NodeTypePathOccupancyTest.cs; reproduced only in the full 1951-test project run. First thing to check per the reporter: whether the shutdown NACK is in the provably-safe re-enqueue set of MeshNodeStreamExtensions.
Origin: https://memex.systemorph.com/rbuergi/Feedback/20260922-1155-repair-write-races-the-type-rebind-recycle (submitted on memex.systemorph.com)
filed by the triage agent on behalf of via systemorph-com
Earlier triage comment (2026-09-23):
Confirmed from the tree — this answers the filing's "first thing to check" — and leaving open: the fix belongs in the busiest file of the write path and needs its own repro before it is changed.
The shutdown refusal is NOT in the re-enqueue set. src/MeshWeaver.Mesh.Contract/MeshNodeStreamExtensions.cs re-enqueues (up to MaxOwnerDisposingReenqueues = 2) only a PatchDataResponse.NodeError whose code is OwnerDisposing, OwnerNotReady or Conflict — the owner's own statement that the patch never applied. The refusal quoted here (Hub … is shutting down (RunLevel=DisposeHostedHubs, activation …) — cannot process RawJson; the address may reactivate) is a DeliveryFailure minted by MessageService before the owner's action block ever sees the patch. In UpdateRemote both arms that receive it — the prompt responseObservable error arm (DeliveryFailureException → MeshNodeErrorCode.Unknown) and the late failure => arm (LATE_DELIVERY_FAILURE → Unknown) — special-case only ErrorType.Unauthorized; ErrorType.ShuttingDown falls through to Unknown. So the second retype's write is refused as Unknown although it provably never applied, exactly as filed.
Why not a one-line "treat ShuttingDown as OwnerDisposing". Doc/Architecture/RidingOutAShuttingDownAddress records that an immediate re-ask routes to the SAME dying activation (it stays registered in its parent until ShutDown), and that a join on its DisposalCompleted can be satisfied instantly by a Dead activation — "three chances inside one millisecond are one chance". The two existing riders (point read, sync stream) bound activation and time separately for that reason. A write rider needs the same two bounds and the activation id the NACK already carries; re-enqueueing without them would spend MaxOwnerDisposingReenqueues inside one teardown and still answer Unknown.
Shape of the fix: in UpdateRemote, classify DeliveryFailure{ErrorType.ShuttingDown} as a provably-unapplied refusal and re-enqueue it through the same ownerSaidNeverApplied path as OwnerDisposing, charging a DIFFERENT activation against the activation budget and pacing a same-activation refusal against a time budget — plus a repro that retypes and writes back-to-back (the filer's NodeTypePathOccupancyTest step, removed from that test, is the natural seed). Not established: whether an ordinary caller (MCP update, the editor) can land inside the window in practice.
Activity
rbuergi commentedon Oct 8, 2026
The defect this filing describes was fixed on 2026-09-28 by
210a5494ad("a write refused as ShuttingDown is re-driven, not lost", #5011 / Plugins#2403). That landed after the 09-23 triage comment above, which is why this issue is still open.What changed, in
src/MeshWeaver.Mesh.Contract/MeshNodeStreamExtensions.cs:IsShuttingDownRefusalis now the ONE classification of aDeliveryFailure{ErrorType.ShuttingDown}.UpdateRemotearms named in the triage send that refusal down the existing OwnerDisposing re-enqueue withownerSaidNeverApplied: trueand the sameMaxOwnerDisposingReenqueuesbudget. These are the prompt response-wait error arm (OWNER_SHUTTING_DOWN_REENQUEUE) and the late arm (LATE_SHUTTING_DOWN_REENQUEUE).Hub … is shutting down (RunLevel=DisposeHostedHubs …) — Rejecting now, answeredUnknown) is no longer terminal.MeshNode Unknown … Rejecting nowbefore that fix.Not established, so I'm leaving this open rather than closing:
NodeTypePathOccupancyTest).Unknown. The fix bounds attempts, not activations or time.If someone re-adds the racing step to
NodeTypePathOccupancyTestand it passes in the full project run, close this. If it fails, that measures concern 2.(Seen while working the nodeops reply cluster, #6295. Unrelated to that change.)
test(#6351): immutable lookups in the lane tests; say which test pins…
rbuergi commentedon Oct 9, 2026
Closing. The close condition from the 10-08 comment is met.
That condition was: re-add the racing step to
NodeTypePathOccupancyTestand have it pass in the full project run. #6357 (merged) does this asUpsert_RepairIssuedStraightAfterAStrandingRetype_Lands:Markdown, without waiting for the rebind recycle.The repair lands. The test passed in the full
MeshWeaver.Graph.Testrun, 2590 / 2590, which is the run that failed on 2026-09-22, and it is green in CI on the PR.This fact is the end-to-end repro, not the deterministic pin. Whether the second write overlaps the teardown is timing. The re-drive of a write refused as ShuttingDown (#5011,
210a5494ad) is pinned deterministically inMeshWeaver.FaultInjection.Test(FleetWatchFreezeTests, usingmesh.Linger(…)), and that test failed with exactly thisUnknown … Rejecting nowbefore the fix.Not established, and carried over from the 10-08 comment: whether two re-enqueues spent inside one long teardown can still end at
Unknown. That is the activations-versus-attempts bound. No production occurrence of this shape has ever been recorded.