You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
QueuedWriteAdvancesOnHandoffTest: the successor's caller can miss its terminal when the first write's ack is late (1/330, Plugins shard 3) #3363
Plugins run 33983731352 (PR MeshWeaver.Plugins#1371, Portal hosts (shard 3)), migrated suite src/MeshWeaver.Hosting.Monolith.Test:
Failed MeshWeaver.Hosting.Monolith.Test.QueuedWriteAdvancesOnHandoffTest.SecondQueuedWrite_WhenTheFirstsAckIsLate_DiffsAgainstTheFirstAndLands [2 s]
MeshWeaver.Reactive.Assertions.AssertionException : Expected value not to be <null> because the successor's caller must receive its terminal.
at QueuedWriteAdvancesOnHandoffTest.SecondQueuedWrite_WhenTheFirstsAckIsLate_DiffsAgainstTheFirstAndLands() in src/MeshWeaver.Hosting.Monolith.Test/QueuedWriteAdvancesOnHandoffTest.cs:line 187
329 of 330 passed in the same assembly. The PR under test moves platform pins (3.1.0-ci.7832 → 3.1.0-ci.7835); between those two core commits (51a39c52f..043f49ede) the only change is #3362 (the ContainerImages rename), and the Plugins diff since the previous green pin run (896043eac..3e81b039) is #1368 and #1369 (kernel-notice i18n). Neither reaches the queued-write handoff, so the diff cannot be the cause: this is a race in the handoff's terminal delivery, sampled once.
Why it is filed rather than re-run
The rule is that a flake is a real race, and re-running to see hides it. The failing assertion is the successor's caller never receiving its terminal after the first write's ack arrives late — the same family #2882/#2890 closed ("the write registers its response callback BEFORE posting"), which suggests a second ordering hole on the successor's side rather than the first write's. The test is a migrated suite, so only MeshWeaver.Plugins runs it; the code under test (MeshWeaver.Hosting queued writes / handoff) is here.
No entry in .github/known-flakes.json: the pattern would be the assertion message above, and it should not be added until the ordering is understood — an allowance is not a fix.
Closed — fixed in MeshWeaver.Plugins#1372, and it was the test, not the handoff
Verified on Plugins main: the two settle signals are present in QueuedWriteAdvancesOnHandoffTest.
Not a second ordering hole on the successor's side. The write was fine in the failing run — the
store poll that precedes the failing assertion had already converged on name="second"with
the description, and secondError was null. Only the caller's terminal had not arrived yet, and the
test read it synchronously the instant the store converged.
Those are two decoupled paths, and MeshNodeStreamCache documents them as such:
the per-path Update queue advances on a bounded signal (QueueAdvanceBound), while
"each write's subscription to the owner's PatchDataResponse must keep running to deliver the
real terminal (value / RLS denial) to its caller", and
UpdateRemote bounds its own owner-response wait at UpdateResponseWaitBound (~2 s) before
falling back to an optimistic emit.
So the owner can have written storage, and the 100 ms poll can observe it, while the terminal is
still in flight. The failing run took [2 s] — UpdateResponseWaitBound exactly. That is the
1-in-330 window.
The fix, and why it is not a widened bound
Each write now completes its own AsyncSubject<Unit> from the subscription's onNextand onError, awaited before any terminal is read — the producer→test signal shape AGENTS.md
prescribes.
🚨 The assertion is not weakened. If a terminal never arrives, the Within(TestTimeouts.Convergence) wait still fails — and now fails saying the caller was never
settled, rather than NotBeNull at whatever instant the store happened to converge. Late and never were indistinguishable before; they are not now. That distinction is the whole difference
between this and raising a timeout.
No entry was added to a known-flakes file, per this issue's own instruction that "an allowance is
not a fix".
On the filing
The attribution in the issue was right and saved time: neither #3362 (a rename nothing referenced)
nor #1369 (two Blazor render paths) can reach the queued-write handoff, and I confirmed that
independently before looking anywhere else.
Investigated. Static reading of UpdateRemote and MeshNodeStreamCache's per-path queue finds no path that delivers a null terminal to a caller whose error is null: every OnNext carries the lambda's output (EmitTerminal), the base node (the two no-op branches), or a re-attempt's value (ChainTerminal); the base sources filter nulls (BaseStateSourceWhere(change.Value is not null)); a null through the typed-content Select would throw into OnError; and the assertion helper's Emit() fails on completion without a value. 40 runs of the test locally against core main (34c3eecec+): 40/40.
So the next occurrence has to name its sequence. MeshWeaver.Plugins#1391 makes both callers record every notification they receive (OnNext with name/description/version, OnError with type and message, OnCompleted) and carries the two sequences in the failure messages. Not a fix; the fix needs that sequence.
Activity
rbuergi commentedon Sep 5, 2026
Closed — fixed in MeshWeaver.Plugins#1372, and it was the test, not the handoff
Verified on Plugins
main: the two settle signals are present inQueuedWriteAdvancesOnHandoffTest.Not a second ordering hole on the successor's side. The write was fine in the failing run — the
store poll that precedes the failing assertion had already converged on
name="second"withthe description, and
secondErrorwas null. Only the caller's terminal had not arrived yet, and thetest read it synchronously the instant the store converged.
Those are two decoupled paths, and
MeshNodeStreamCachedocuments them as such:QueueAdvanceBound), whilePatchDataResponsemust keep running to deliver thereal terminal (value / RLS denial) to its caller", and
UpdateRemotebounds its own owner-response wait atUpdateResponseWaitBound(~2 s) beforefalling back to an optimistic emit.
So the owner can have written storage, and the 100 ms poll can observe it, while the terminal is
still in flight. The failing run took
[2 s]—UpdateResponseWaitBoundexactly. That is the1-in-330 window.
The fix, and why it is not a widened bound
Each write now completes its own
AsyncSubject<Unit>from the subscription'sonNextandonError, awaited before any terminal is read — the producer→test signal shape AGENTS.mdprescribes.
🚨 The assertion is not weakened. If a terminal never arrives, the
Within(TestTimeouts.Convergence)wait still fails — and now fails saying the caller was neversettled, rather than
NotBeNullat whatever instant the store happened to converge. Late andnever were indistinguishable before; they are not now. That distinction is the whole difference
between this and raising a timeout.
No entry was added to a known-flakes file, per this issue's own instruction that "an allowance is
not a fix".
On the filing
The attribution in the issue was right and saved time: neither #3362 (a rename nothing referenced)
nor #1369 (two Blazor render paths) can reach the queued-write handoff, and I confirmed that
independently before looking anywhere else.
rbuergi commentedon Sep 6, 2026
Investigated. Static reading of
UpdateRemoteandMeshNodeStreamCache's per-path queue finds no path that delivers a null terminal to a caller whose error is null: everyOnNextcarries the lambda's output (EmitTerminal), the base node (the two no-op branches), or a re-attempt's value (ChainTerminal); the base sources filter nulls (BaseStateSourceWhere(change.Value is not null)); a null through the typed-contentSelectwould throw intoOnError; and the assertion helper'sEmit()fails on completion without a value. 40 runs of the test locally against core main (34c3eecec+): 40/40.So the next occurrence has to name its sequence. MeshWeaver.Plugins#1391 makes both callers record every notification they receive (OnNext with name/description/version, OnError with type and message, OnCompleted) and carries the two sequences in the failure messages. Not a fix; the fix needs that sequence.