Skip to content

Second write racing a retype's hub recycle is refused as Unknown instead of retry-this #6034

Closed

Description

@rbuergi

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

  1. added
    bugSomething isn't working
    sev:MMedium - secondary path broken, or easy workaround
    area:meshCore subsystem: the mesh - nodes, partitions, addressing, activation
    on Oct 3, 2026
  2. rbuergi commented on Oct 8, 2026

    @rbuergi
    ContributorAuthor

    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:

    • IsShuttingDownRefusal is now the ONE classification of a DeliveryFailure{ErrorType.ShuttingDown}.
    • Both UpdateRemote arms named in the triage send that refusal down the existing OwnerDisposing re-enqueue with ownerSaidNeverApplied: true and the same MaxOwnerDisposingReenqueues budget. These are the prompt response-wait error arm (OWNER_SHUTTING_DOWN_REENQUEUE) and the late arm (LATE_SHUTTING_DOWN_REENQUEUE).
    • So the exact refusal quoted here (Hub … is shutting down (RunLevel=DisposeHostedHubs …) — Rejecting now, answered Unknown) is no longer terminal.
    • Its tests are in MeshWeaver.Hosting.Orleans.Test. The "lingering stop" case failed with exactly this MeshNode Unknown … Rejecting now before that fix.

    Not established, so I'm leaving this open rather than closing:

    1. No production evidence existed for this filing, so there is nothing on the deployed portal to verify against. I did not re-run the original back-to-back retype repro (NodeTypePathOccupancyTest).
    2. The triage's remaining concern is not measured. That concern is that an immediate re-ask can land on the SAME dying activation, so two re-enqueues spent inside one teardown still end at Unknown. The fix bounds attempts, not activations or time.

    If someone re-adds the racing step to NodeTypePathOccupancyTest and 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.)

  3. added a commit that references this issue on Oct 9, 2026
  4. rbuergi commented on Oct 9, 2026

    @rbuergi
    ContributorAuthor

    Closing. The close condition from the 10-08 comment is met.

    That condition was: re-add the racing step to NodeTypePathOccupancyTest and have it pass in the full project run. #6357 (merged) does this as Upsert_RepairIssuedStraightAfterAStrandingRetype_Lands:

    1. strand a node by retyping it onto an occupied path through the escape hatch;
    2. immediately upsert it back to Markdown, without waiting for the rebind recycle.

    The repair lands. The test passed in the full MeshWeaver.Graph.Test run, 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 in MeshWeaver.FaultInjection.Test (FleetWatchFreezeTests, using mesh.Linger(…)), and that test failed with exactly this Unknown … Rejecting now before 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:meshCore subsystem: the mesh - nodes, partitions, addressing, activationbugSomething isn't workingfeedbacksev:MMedium - secondary path broken, or easy workaround

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions