Repository navigation
Description
A response to an IN-FLIGHT request is refused during teardown, so the caller hangs
MeshWeaver.Graph.Test.NackReachesTheWaiterDuringTeardownTest.OwnerDisposingUnderMeshTeardown_StillAnswersTheWaitingCaller
fails intermittently with System.TimeoutException. It has now blocked two pull requests and
rejected a merge-queue group build, so core cannot merge reliably while it stands.
The mechanism, from the test's own trace
[write] patch posted with marker teardown-nack-4de6af5289; owner merge is parked
[fence] caller is armed on the late watch (ArmedCount=1)
[dispose] mesh disposal invoked — parent is past DisposeHostedHubs
[Warning] Message delivery failed for PatchDataResponse (ID: JKbVdl_ttECug_3A1Y2hjQ)
in TestData/teardown-nack-node: Hub TestData/teardown-nack-node cannot route
PatchDataResponse to cache/mFUWHEUx… — its parent hub mesh/mFUWHEUx… is shutting
down (RunLevel=DisposeHostedHubs). The address may reactivate (recycle / restart);
retry to get the authoritative answer.
Three deliveries are refused the same way in the same millisecond — DataChangedEvent ×2,
PatchDataResponse, and a GetDataResponse to client/8ae4ca32e32b.
The refusal does not distinguish new work from an answer owed to a caller already waiting. The
guard's own text — "retry to get the authoritative answer" — is advice a request sender can
act on. A response has no such option: the caller is parked on the reply, the request will never
be re-sent, and the reply is the thing being dropped. So the caller waits out its budget for an
answer the owner believes it sent. PatchDataResponse and GetDataResponse are answers, not work.
Why this is NOT a flake, and must not be catalogued as one
I nearly added it to .github/known-flakes.json to get a delivery-blocking fix through, and that
would have been wrong: cataloguing teaches the steward to re-queue this failure fleet-wide, and
the failure is real. The test is a detector doing its job — the name says exactly what it asserts
("still answers the waiting caller"), and the log shows the answer being dropped. What is
intermittent is the RACE (whether disposal reaches DisposeHostedHubs before the response is
routed), not the correctness of the complaint.
Almost certainly the same family as #3298
PodHubTransportTest.CrossSiloNack_ReachesASenderWhoseStreamSubscriptionIsGone fails the same way
one layer out: a NACK that must reach a sender during teardown does not arrive, and the requester
spends its whole budget. Different assembly, different transport, same sentence — an answer owed to
a waiting caller is dropped once the owner is shutting down. Worth investigating together; I have
not proven they share a root cause, and I falsified my first attempt at linking #3298 to a
disposal change, so
treat the connection as a hypothesis.
What I ruled out
- Newly-introduced test. No — added 2026-09-02 by
51c158bd9 fix: the patch-ack completion arm stands aside for a shutting-down owner, 357 commits back. - Caused by Teardown lets accepted work finish and names what it had to stop — no forced teardown #3291 (
fix/robust-disposal, merged 12:26Z). Not established. A tree containing
Teardown lets accepted work finish and names what it had to stop — no forced teardown #3291 passed the sibling shard, and a tree without it failed Flake: PodHubTransportTest.CrossSiloNack_ReachesASenderWhoseStreamSubscriptionIsGone — red on shard 0 with a control arm that isolates it #3298's test. Same-window
correlation is not evidence.
Suggested direction
DisposeHostedHubs should stand aside for replies to in-flight requests the way the commit that
added this test made the patch-ack arm stand aside — the shutdown guard is right to refuse new work
and wrong to refuse an answer. Evidence:
- queue build https://github.com/Systemorph/MeshWeaver/actions/runs/33877631698 (job 101039142435, shard 4), which dequeued fix(ci): the bundle identity anchor is a platform property, not a module's reference graph #3293 with
queue-rejected - the same test also blocks fix(i18n): the provider page must not claim an unpriced model is unbilled #3297
Activity
rbuergi commentedon Sep 4, 2026
The hypothesis in the issue is CONFIRMED, and the fix already landed
Root cause, verified end-to-end.
HierarchicalRouting.RouteAlongHostingHierarchyrefuses everydelivery it would have to hand to a parent past
DisposeHostedHubsand returnsdelivery.Failed(…, ShuttingDown). That failure is reported byMessageService.ReportFailure,which — like every other failure route here — answers
delivery.Sender. For a REQUEST that isexact. For a reply the sender is the RESPONDER: it is waiting for nothing, will never re-send,
and the party actually parked on the message is never told. Two of
ReportFailure's own exits thendrop it in silence (the
RunLevel >= DisposeHostedHubsgate, andMayAnswer()— which asks whetherthe SENDER wants an answer, not the question a reply poses).
The refusal's own advice — "retry to get the authoritative answer" — is a claim about its
addressee, and on a reply that claim is false. That is why this read correctly in the source for
as long as it did.
Why #3196's claim-then-verify could not catch it.
AckOncelatches its once-only gate beforeposting, which disables
RegisterOwnerDisposingNack's sink dispatch;PostPatchVerdictcompensatesby falling through to the sink when the post is REFUSED. But it reads the delivery
hub.Postreturned, and here the post is ACCEPTED — the owner's own run level is still open. The refusal
happens a turn later in routing, invisible to that seam.
RoutePatchVerdict's own remarks name thegap and locate the fix exactly where it landed:
Why it lands only under load.
Mesh.Dispose()synchronously cascadesCloseCreationthroughthe whole subtree — every descendant's
IsShuttingDownflips while the mesh is still atStarted—then posts
Quiescingto its own loop. The parked merge turn wakes on that first signal, and itsack is not one step either:
hub.Postis evaluated on the owner's turn and ROUTED on a later one.On an idle machine the merge wins; on a loaded shard the mesh reached
DisposeHostedHubsin 3 ms.I measured 15/15 local passes with the defect fully present.
What landed
684cc6ac1(#3302) —IUndeliverableReplySink, offered byHierarchicalRoutingon the two armswhere it gives up, implemented by
LatePatchResponseRegistryas a factory over the same singleton.Its branch is named for a quiescing gate that was dropped before merge; the change that shipped is
the hand-over alone. Core CD is running on it now, first delivery since 10:23Z.
What was still missing, and is in #3309
The fix shipped with no guard that can fail. Its only cover is
NackReachesTheWaiterDuringTeardownTest— the detector that found this — which reaches the state byracing. Measured, with both
HierarchicalRoutingcall sites disabled and the two tests runtogether:
Passed: 1is the field detector, with the production fix removed. It printed the same refusalit complains about in CI and passed anyway.
RefusedReplyReachesTheWaiterTestholds the state open by construction — the owner's action blockis parked on an accepted turn, so its own
ShutdownRequestqueues behind that turn and the ownercannot leave
Started, while the parent cannot leaveDisposeHostedHubsuntil the owner finishes.Both halves are fenced on. A LIVE waiter hub gives it two controls that do not depend on which layer
performs the hand-over: a probe proves the route carries this message before the teardown, and the
waiter's silence afterwards proves the reply never travelled.
Also there:
UndeliverableReplyShapesTest(which shapes the registry takes — including theDeliveryFailurearm of #2661, which nothing else covered), andDoc/Architecture/RefusedRepliesDuringTeardown, which states the asymmetry as a rule so the nextrefusal, intake gate or teardown drop asks who is waiting, not who sent it.
#3298 is a different defect — see the comment there
Same sentence, different mechanism, on three independent counts. Not fixed by this, and it stays
open.
test(#3303): key the watch assertions on THIS request id, not on the …
rbuergi commentedon Sep 4, 2026
Closing — the defect is fixed in
main, and I measured it rather than assuming it684cc6ac1(#3302) carries the hand-over. Verified on the merged tree, not on the PR:And proved that it is what closes THIS defect, by removing it: with both call sites disabled the
deterministic reproduction fails on exactly the sentence in this issue, and restoring them makes it
pass in under a second. Core CD is running on
684cc6ac1— the first delivery since 10:23Z.The hypothesis in the issue body was right, including the part that mattered most: the guard's own
advice, "retry to get the authoritative answer", is a claim about its ADDRESSEE, and on a reply
that addressee is the responder rather than the caller. That is why it read correctly in source for
as long as it did.
#3309 adds what the fix shipped without:
RefusedReplyReachesTheWaiterTest(the state held openby construction, so it fails without the race),
UndeliverableReplyShapesTest(theDeliveryFailurearm of #2661, which nothing else covered), and
Doc/Architecture/RefusedRepliesDuringTeardown. Thatis hardening on top of a fix that is already in
main, so it does not hold this issue open.#3298 is not this — different funnel (
RoutingGrain.PostFailure, neverHierarchicalRouting),different precondition (nothing is shutting down), different waiter (a
RegisterStreamcallback onanother silo, not an armed registry entry). It stays open; details in the comment there.