Skip to content

QueuedWriteAdvancesOnHandoffTest: the successor's caller can miss its terminal when the first write's ack is late (1/330, Plugins shard 3) #3363

Closed

Description

@rbuergi

Observed

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.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JuXoLRvG9TfKh5mgnjGfmo

Activity

  1. rbuergi commented on Sep 5, 2026

    @rbuergi
    ContributorAuthor

    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 onNext and
    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.

  2. rbuergi commented on Sep 6, 2026

    @rbuergi
    ContributorAuthor

    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 (BaseStateSource Where(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.

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

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions