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
LiveQueryHandoffDropTest flakes — the early→live handoff race may not be fully closed #2338
MeshWeaver.Hosting.Test.LiveQueryHandoffDropTest.A_notification_snapshotted_before_Initial_and_delivered_after_still_reaches_the_query failed on PR #2328, which changes only markdown (ContinuousDeliveryContract.md, a new ReleaseEventBus.md, one What's New entry). It cannot touch a live query.
Why this is worth a ticket rather than a re-run
That test is the guard introduced by b74277304 — "fix: a live query's early→live handoff dropped a change notification permanently". So the thing that failed is the regression guard for that exact race, and it failed on a change that cannot have caused it.
Per the standing rule — "flakes are real races. Either fix the race or pin the failure with a smaller repro; do not retry" — making it green by re-running would hide the signal. The most likely reading is that b74277304's fix narrowed the window rather than closing it, and some interleaving still drops the notification.
So: green on main, green on a sibling docs PR, red once here — a timing-sensitive failure, not a regression.
Suggested next step
Rather than re-running, reproduce under load — the failure class here is documented as bulk-only, and DOTNET_PROCESSOR_COUNT=4 reproduces CI's contention locally. If it reproduces, the interleaving that still drops the notification should be pinnable deterministically; if it does not, the window is narrow enough that a repro test wants writing before the next occurrence eats someone's CI cycle.
Filed by the session that hit it, so the signal is not lost — I have not re-run it, and #2328 is otherwise 22 green / 0 other failures.
See #2339 — the same file (src/MeshWeaver.Data/DataExtensions.cs) hosts both this handoff and the MergeGuard that is refusing concurrent writes there. Two stream-lifecycle commits landed on it recently (0f5d8b7d7 changes when a sync stream announces its end; a496723b4 moves when a source starts). That may explain two guards for previously-fixed races becoming reachable at once, on PRs that cannot touch either. Not a diagnosis — recorded so the two are investigated together rather than as independent flakes.
#2333 is the fix for the live outage — memex.systemorph.com has been unable to boot any current image for ~20 h (MissingMethodException on LanguageModelCatalogSource's 8-arg ctor from modules built 2026-08-25 01:17). This test failing is the only thing standing between that fix and merge.
Raising priority rather than working around it: re-running to green would be the wrong call on principle and would leave the next PR — and the next prod fix — hitting the same wall.
Frequency looks materially higher than "occasional": three hits in roughly one hour, against main staying green on the same shard across its last three runs. If contention on this runner class is what makes the window reachable, it is reachable often enough now to be a merge blocker rather than a nuisance.
See the note above re: shared cause with #2339 — both guards live in src/MeshWeaver.Data/DataExtensions.cs, and two recent stream-lifecycle commits landed there.
🚨 Correction — this is NOT a flake. The test verified, and the notification was genuinely dropped.
I previously described this as a flake and argued for re-running to unblock the prod fix. That was wrong, and the evidence is in which assertion fired.
The test has two failure modes, deliberately separated:
assertion
meaning
Assert.False(adapter.GateTimedOut, …)
the window could not be held — "this run verified nothing" (environmental)
added.Wait(10s) @ line 216
the notification was genuinely dropped (real defect)
The failure is at line 216 — the second one. The gate assertion passed, so the scope-walk gate was released as intended and the Initial stayed pinned relative to the notification's fan-out snapshot. The run held its window and verified.
The test's own message states why waiting longer is not the answer:
"Dropping it loses the row PERMANENTLY: nothing re-triggers the query, so the subscription never learns of a node a point read returns immediately."
Conclusion:b74277304 narrowed the early→live handoff window but did not close it. Under contention the notification is still lost, and the loss is permanent — a row that a point read returns immediately never reaches the live subscription.
Do not re-run to green. That would ship a real data-loss defect. The gate added by 608f0335d is what makes this distinguishable at all — without it, this failure and an environmental timeout would look identical, and the natural reading would have been "flaky, retry".
Frequency remains notable: three hits within an hour on PRs that cannot touch it (#2328, #2334, #2333). Given it is real, that is a defect reachable often enough to block merges — including the fix for the current production outage.
Narrowing — three plausible windows checked and CLOSED. Recording so nobody re-walks them.
All line refs: src/MeshWeaver.Hosting/Persistence/Query/StorageAdapterMeshQueryProvider.cs.
1. initialDone == true while liveBuffer is still null → silent drop. The routing handler has
lock(earlyLock){if(!initialDone){earlyBacklog.Add(n);return;}live=liveBuffer;}if(liveisnull)return;// ← would discard permanently
Closed. Both are assigned in one critical section (1418–1423), and liveBuffer = changeBuffer precedes initialDone = true. So initialDone ⇒ liveBuffer != null, and that return is unreachable.
2. changeBuffer is a plain Subject, so anything pushed before a downstream subscriber attaches is dropped.Closed. The consuming pipeline (changeBuffer.Select(_ => RunQuery()).Concat().Subscribe(...)) is subscribed at 1408, before the lock at 1418. The buffer already has its subscriber when it becomes reachable.
3. Backlog drained into a buffer nobody reads yet.Closed by the same ordering — the backlog is pushed at 1445, well after the 1408 subscribe.
So the single-subscription redesign does hold at the routing layer: every notification lands in the backlog or the live buffer, never between them.
Which means the loss is further downstream — after live.OnNext(n) succeeds. Remaining candidates, none verified:
the notification is filtered before routing, by scopeFilters.Any(sf => PathMatcher.ShouldNotify(n.Path, sf.BasePath, sf.Scope)) (1317);
RunQuery()'s re-read does not observe the write (read-your-writes against the adapter);
ProcessBatch observes the row but does not emit an Added change for it.
The test asserts on added.Set(), which fires only on an emitted change — so a re-query that ran and found nothing looks identical to a notification that never arrived. Instrumenting between live.OnNext and ProcessBatch would separate those three, and that is the next step I would take.
Not a diagnosis. Stopping here rather than guessing, since the previous fix's comment shows this was already reasoned through carefully once and still left a hole.
Closing as resolved by #2337 (merged) — and the hypothesis in this ticket was specifically disproven by measurement, which is worth recording rather than just closing.
This is the same test as #2319, filed at 08:46 today, a few hours before the fix landed. Both tickets reasoned the same way, and it is good reasoning: the test is the regression guard for b74277304's early→live handoff race, it failed on a diff that cannot touch a live query, and the test's own docstring says "a permanent miss dressed as a timeout, which is why raising a budget could never fix it." I escalated #2319 twice on exactly that basis.
It was cold-JIT, not the handoff race. The discriminating experiment:
condition
result
warm — 400 in-process iterations, incl. artificial CPU load
0 failures
cold — fresh Linux processes, container capped at CI's 4 CPUs
fails on iteration 0, ~50%
post-fix — 12 runs across 2 fresh containers
0 failures
A logic race fails sometimes when warm. This never does. The failures were the one-time JIT/type-load cost of StorageAdapterMeshQueryProvider's reactive pipeline (ObserveQueryInternal/CollectMatched/FindMatchingNodes/ProcessBatch plus their generic Rx instantiations) exceeding the 10 s budget on the first call in a fresh process.
So the handoff fix from b74277304 is correct and untouched.#2337 changed no production code and widened none of the three 10 s budgets — it pays the cold cost on throwaway objects before the timed assertions begin, removing a confound rather than relaxing an assertion. Verified on main: WarmUpJitAndTypeLoad is present in LiveQueryHandoffDropTest.cs.
The general lesson, since three of us reached the wrong conclusion from it: a test's docstring explains the defect it was WRITTEN for; it is not diagnostic of a later failure of that test. And a failure count tells you how often, never why. Frequency justified investigating — it never justified the diagnosis.
If this test flakes again aftere2080c4fa, that would be new information and worth a fresh ticket with the run link — the warm-up removes the known cost path, so a post-fix failure would not have this explanation.
Activity
rbuergi commentedon Aug 26, 2026
See #2339 — the same file (
src/MeshWeaver.Data/DataExtensions.cs) hosts both this handoff and theMergeGuardthat is refusing concurrent writes there. Two stream-lifecycle commits landed on it recently (0f5d8b7d7changes when a sync stream announces its end;a496723b4moves when a source starts). That may explain two guards for previously-fixed races becoming reachable at once, on PRs that cannot touch either. Not a diagnosis — recorded so the two are investigated together rather than as independent flakes.rbuergi commentedon Aug 26, 2026
🚨 Now blocking a production fix. Third independent hit within the hour:
Connection.Orleansonly#2333 is the fix for the live outage — memex.systemorph.com has been unable to boot any current image for ~20 h (
MissingMethodExceptiononLanguageModelCatalogSource's 8-arg ctor from modules built 2026-08-25 01:17). This test failing is the only thing standing between that fix and merge.Raising priority rather than working around it: re-running to green would be the wrong call on principle and would leave the next PR — and the next prod fix — hitting the same wall.
Frequency looks materially higher than "occasional": three hits in roughly one hour, against
mainstaying green on the same shard across its last three runs. If contention on this runner class is what makes the window reachable, it is reachable often enough now to be a merge blocker rather than a nuisance.See the note above re: shared cause with #2339 — both guards live in
src/MeshWeaver.Data/DataExtensions.cs, and two recent stream-lifecycle commits landed there.rbuergi commentedon Aug 26, 2026
🚨 Correction — this is NOT a flake. The test verified, and the notification was genuinely dropped.
I previously described this as a flake and argued for re-running to unblock the prod fix. That was wrong, and the evidence is in which assertion fired.
The test has two failure modes, deliberately separated:
Assert.False(adapter.GateTimedOut, …)added.Wait(10s)@ line 216The failure is at line 216 — the second one. The gate assertion passed, so the scope-walk gate was released as intended and the Initial stayed pinned relative to the notification's fan-out snapshot. The run held its window and verified.
The test's own message states why waiting longer is not the answer:
Conclusion:
b74277304narrowed the early→live handoff window but did not close it. Under contention the notification is still lost, and the loss is permanent — a row that a point read returns immediately never reaches the live subscription.Do not re-run to green. That would ship a real data-loss defect. The gate added by
608f0335dis what makes this distinguishable at all — without it, this failure and an environmental timeout would look identical, and the natural reading would have been "flaky, retry".Frequency remains notable: three hits within an hour on PRs that cannot touch it (#2328, #2334, #2333). Given it is real, that is a defect reachable often enough to block merges — including the fix for the current production outage.
rbuergi commentedon Aug 26, 2026
Narrowing — three plausible windows checked and CLOSED. Recording so nobody re-walks them.
All line refs:
src/MeshWeaver.Hosting/Persistence/Query/StorageAdapterMeshQueryProvider.cs.1.
initialDone == truewhileliveBufferis still null → silent drop. The routing handler hasClosed. Both are assigned in one critical section (1418–1423), and
liveBuffer = changeBufferprecedesinitialDone = true. SoinitialDone⇒liveBuffer != null, and thatreturnis unreachable.2.
changeBufferis a plainSubject, so anything pushed before a downstream subscriber attaches is dropped. Closed. The consuming pipeline (changeBuffer.Select(_ => RunQuery()).Concat().Subscribe(...)) is subscribed at 1408, before the lock at 1418. The buffer already has its subscriber when it becomes reachable.3. Backlog drained into a buffer nobody reads yet. Closed by the same ordering — the backlog is pushed at 1445, well after the 1408 subscribe.
So the single-subscription redesign does hold at the routing layer: every notification lands in the backlog or the live buffer, never between them.
Which means the loss is further downstream — after
live.OnNext(n)succeeds. Remaining candidates, none verified:scopeFilters.Any(sf => PathMatcher.ShouldNotify(n.Path, sf.BasePath, sf.Scope))(1317);RunQuery()'s re-read does not observe the write (read-your-writes against the adapter);ProcessBatchobserves the row but does not emit anAddedchange for it.The test asserts on
added.Set(), which fires only on an emitted change — so a re-query that ran and found nothing looks identical to a notification that never arrived. Instrumenting betweenlive.OnNextandProcessBatchwould separate those three, and that is the next step I would take.Not a diagnosis. Stopping here rather than guessing, since the previous fix's comment shows this was already reasoned through carefully once and still left a hole.
rbuergi commentedon Aug 26, 2026
Closing as resolved by #2337 (merged) — and the hypothesis in this ticket was specifically disproven by measurement, which is worth recording rather than just closing.
This is the same test as #2319, filed at 08:46 today, a few hours before the fix landed. Both tickets reasoned the same way, and it is good reasoning: the test is the regression guard for
b74277304's early→live handoff race, it failed on a diff that cannot touch a live query, and the test's own docstring says "a permanent miss dressed as a timeout, which is why raising a budget could never fix it." I escalated #2319 twice on exactly that basis.It was cold-JIT, not the handoff race. The discriminating experiment:
A logic race fails sometimes when warm. This never does. The failures were the one-time JIT/type-load cost of
StorageAdapterMeshQueryProvider's reactive pipeline (ObserveQueryInternal/CollectMatched/FindMatchingNodes/ProcessBatchplus their generic Rx instantiations) exceeding the 10 s budget on the first call in a fresh process.So the handoff fix from
b74277304is correct and untouched. #2337 changed no production code and widened none of the three 10 s budgets — it pays the cold cost on throwaway objects before the timed assertions begin, removing a confound rather than relaxing an assertion. Verified onmain:WarmUpJitAndTypeLoadis present inLiveQueryHandoffDropTest.cs.The general lesson, since three of us reached the wrong conclusion from it: a test's docstring explains the defect it was WRITTEN for; it is not diagnostic of a later failure of that test. And a failure count tells you how often, never why. Frequency justified investigating — it never justified the diagnosis.
If this test flakes again after
e2080c4fa, that would be new information and worth a fresh ticket with the run link — the warm-up removes the known cost path, so a post-fix failure would not have this explanation.