Skip to content

#2778 has recurred: a minted OwnerDisposing NACK reaches a waiter whose watch is still armed after the ShutDown registrant ran — 2 sightings, the catch-path hypothesis eliminated #4023

Closed

Description

@rbuergi

Summary

NackReachesTheWaiterDuringTeardownTest.OwnerDisposingUnderMeshTeardown_StillAnswersTheWaitingCaller
has now failed twice, and its own assertion names the cause: #2778's defect is back. That
issue is CLOSED and nothing open tracks the recurrence.

The two sightings

# when where
1 recorded in #3965's doc run 34513634943, shard 4
2 2026-09-11T09:14:14Z run 34581527040, job 103206660798, Portal hosts (shard 2), on MeshWeaver.Plugins PR #1642

🚨 Not the diff. #1642 changes a workflow and a Python comparator script — it cannot reach hub
teardown. Same argument as the pod-hub race: a diff that provably cannot touch the code failed the
test.

The assertion, in full

Did not expect collection {"JX3lvnrHzkybl5Z-NJ4rNQ"} to contain "JX3lvnrHzkybl5Z-NJ4rNQ" because
the owner minted an OwnerDisposing NACK for this patch, and a caller was armed and waiting for
it; the verdict must reach that watch even though the parent is past DisposeHostedHubs. The
owner is now terminally Dead, so its ShutDown-phase registrant has already run — a watch still
armed here is the #2778 defect
, and the caller then hears nothing and burns the whole 31 s
verdict budget for an answer that had already been minted.

🚨 The first hypothesis is ELIMINATED — measured, not argued

Doc/Architecture/DisposalStallVerdicts (written by #3965) names the hypothesis to eliminate first:
HandleShutdownCore reaches Dead on three paths — success, catch, and a finally backstop —
and only the first guarantees the ShutDown-phase registrant ran. It names the discriminator: the
catch's Error during shutdown of hub {address} line.

That line appears 0 times in the failing job's log (126,780 bytes, ANSI-stripped before
grepping). So the catch path did not run, the owner reached Dead cleanly, and the
ShutDown-phase registrant DID run.

Which leaves the remaining explanation, exactly as the assertion states it: the registrant ran and
the watch is still armed afterwards.
That is #2778's shape, not a teardown-path variant.

This also sharpens the family distinction #3965 drew, and it is worth restating because conflating
these two has already cost time:

Why it matters beyond a red shard

The caller burns its entire 31 s verdict budget waiting for an answer that had already been
minted. In CI that is one red test. In production it is a write that was answered and a caller that
never heard — the silent-loss shape, where nothing is red and nothing is logged.

What a fix needs

  1. Find where the watch outlives the registrant. The registrant provably ran, so the defect is
    between minting the verdict and the watch being retired — not in whether teardown reached the
    registrant at all.
  2. 🚨 Do not catalogue it in .github/known-flakes.json. That would teach merge-queue-steward
    to re-queue past a real race. Flakes here are real races — this one has a production twin in its
    own assertion text.
  3. Control in both directions: the armed waiter receives a minted OwnerDisposing verdict under
    mesh teardown, AND an ordinary teardown with nothing owed still retires its watches promptly. A
    fix that simply never retires a watch would pass the first and leak on every teardown.
  4. Re-read OwnerDisposing NACK is dropped exactly when the parent is disposing hosted hubs — the caller hangs for its full budget #2778's original fix and say why it stopped holding — a recurrence whose first fix is
    not understood tends to come back a third time.

Where it lives

The test is in MeshWeaver.Plugins (src/MeshWeaver.Hosting.Monolith.Test/NackReachesTheWaiterDuringTeardownTest.cs:236);
the code under test is core's hub teardown. Filed here because the defect is core's.

Related: #2778 (closed, the original) · #3510 / #4009 (the sibling seam, fixed) ·
#3965 · Doc/Architecture/DisposalStallVerdicts

🤖 Generated with Claude Code

Activity

  1. added 6 commits that reference this issue on Sep 11, 2026
  2. rbuergi commented on Sep 11, 2026

    @rbuergi
    ContributorAuthor

    Status of the caller adoption (requirement 6), 2026-09-11 ~15:00Z.

    Landed in core

    Durable write-up: Doc/Architecture/ModulePublicationGate (caller, both directions, check-callers, landing order, not-solved list) and Doc/Architecture/WorkflowPermissionPairing (pending:).

    Open

    • Systemorph/MeshWeaver.Plugins#1663 switches modules-floor to publish-mode: staged and adds publish-modules. Auto-merge is armed.
      • At 15:11Z, 4 of its 7 required contexts are green (both validate contexts, Repo policy gates, Module bundles / All selected bundles built), and 2 were still running (Compile every NodeType (vs core), test-repos). Build + test the portal hosts is red on shard 3 only, the known red on Plugins main from core set 8345 (NodeTypeRecompileAlcLeakTest). Shards 0 to 2 pass. The fix isn't on Plugins main yet; Plugins#1656 is diagnostics only.
      • Once the fix lands, merge origin/main into fix/3878-staged-module-publication, run python3 scripts/gen-manifests.py, and push. The new run then goes green and auto-merge lands it.
    • After feat(hosting): the endpoint-contribution hook — modules map HTTP routes (#1655) #1663 merges, open the PR from core branch fix/3878-roster-marker-spent (f41b0dc28), which removes the spent marker. Do not open it earlier: I checked that this roster against a Plugins main without the caller fails --assert-fleet-row (exit 1).
    • Production verification is still owed. The first trunk run after feat(hosting): the endpoint-contribution hook — modules map HTTP routes (#1655) #1663 must show publish-modules / Hand the validated bundles to the registry succeeding, with registry 2xx lines for the staged bytes. A PR run exercises neither staging nor the hand-over (publish is false there).

    Not solved, and not claimed

  3. rbuergi commented on Sep 11, 2026

    @rbuergi
    ContributorAuthor

    Fixed — #4036 merged (2026-09-11T15:18Z). Closing; I filed this, and the premise I filed it on was wrong.

    Verified on main by file, not by PR state: src/MeshWeaver.Hosting/RoutingServiceBase.cs carries the
    change, and test/MeshWeaver.Graph.Test/ReplyRoutedDuringMeshTeardownReachesItsWaiterTest.cs is present.
    The queue build ran all six test shards green on the merged tree.

    The correction that matters

    I titled this "#2778 has recurred" and asked why #2778's fix stopped holding. It never stopped holding.

    The actual defect

    RoutingServiceBase.RouteInMesh silently dropped every message once the mesh reached
    DisposeHostedHubs
    ("recipients are likely also disposing") while still reporting Forwarded — the
    "nobody is waiting" assumption at a third site. Failing trail: PATCH_ACK ok → response posted → mesh
    receives it while Quiescing → mesh routes it after its own DisposeHostedHubs turn → never arrives,
    while the cache hub is Quiescing with exactly that callback pending. Monolith and CI only;
    OrleansRoutingService has no such guard.

    Controls — each fails against exactly the variant it guards

    build answer → hosted hub answer → stream recipient uncorrelated still refused
    unfixed router fails fails passes
    fix passes passes passes
    over-broad fix fails fails fails
    no stream hook passes fails passes

    Stress: 0 failures in 1,600 with the fix vs 4 in 3,600 before (supporting only — the deterministic test
    is the proof). Teardown cost unchanged (189 s base vs 187 s branch).

    Left open, deliberately

    The test's own residual race — judging the ordinary route at the owner's Dead — seen once in 3,600
    stressed runs, never in CI. Documented in DisposalStallVerdicts ("Resolved 2026-09-11"), not fixed.

    🤖 Generated with Claude Code

  4. rbuergi commented on Sep 24, 2026

    @rbuergi
    ContributorAuthor

    New sighting, 2026-09-24T15:19Z, in the merge queue. Group build https://github.com/Systemorph/MeshWeaver/actions/runs/36019287652 (gh-readonly-queue/main/pr-5664-…), job Run tests (shard 4):

    MeshWeaver.Graph.Test.NackReachesTheWaiterDuringTeardownTest.OwnerDisposingUnderMeshTeardown_StillAnswersTheWaitingCaller — Did not expect collection {"LW1lBiz2VEaR2HYBc7KHdQ"} to contain "LW1lBiz2VEaR2HYBc7KHdQ" because the owner minted an OwnerDisposing NACK for this patch, and a caller was armed and waiting.

    The group was #5663 (docs only) with #5664 on top. #5664 changes only Memex.Portal.ServiceDefaults, which only Memex.Portal.Shared.Test references, so neither PR can reach Graph.Test. The same test tree passed in the previous group build for #5664, https://github.com/Systemorph/MeshWeaver/actions/runs/36017948638 (success). The steward left #5664 dequeued as queue-rejected.

    Not established: whether this is the #4023 mechanism or a new path to the same assertion. I did not read the shard's trace log. I have not reopened this issue; triage should decide that. (The triage inbox on the control instance was not reachable from this session.)

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions