Repository navigation
Fix the usage-satellite read and close #1029's second door (AI.Test parallel opt-in: measured no) - #1043
Fix the usage-satellite read and close #1029's second door (AI.Test parallel opt-in: measured no)#1043rbuergi wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates ThreadTokenUsageTest to observe the token-usage satellite the same way the portal does (via a live children query), eliminating a parallel-only race where point-reading an absent usage node fails fast with an authoritative NotFound instead of waiting for creation.
Changes:
- Added a new regression test that starts watching usage before any
_Usagesatellite exists, ensuring usage arrives once created. - Reworked
WaitForUsageto use a livepath:{thread}/_Usage scope:childrenquery and select the model’s usage node from the result set (mirroring the portal binding path).
Suppressed comments (1)
test/MeshWeaver.AI.Test/ThreadTokenUsageTest.cs:397
- This XML doc comment references
TokenUsage.RecordUsage, but the implementation isTokenUsageNodeType.RecordUsage. SinceTokenUsage.RecordUsagedoesn't exist, the comment is misleading.
/// <c>TokenUsage.RecordUsage</c>, which is subscribed as an INDEPENDENT side effect and
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Test Results (shard 5)1 769 tests 1 577 ✅ 4m 49s ⏱️ Results for commit 19d6268. ♻️ This comment has been updated with latest results. |
Test Results (shard 3) 11 files ±0 11 suites ±0 9m 59s ⏱️ + 1m 6s For more details on these failures, see this check. Results for commit 19d6268. ± Comparison against base commit 412fc5c. ♻️ This comment has been updated with latest results. |
Test Results 65 files ±0 65 suites ±0 50m 53s ⏱️ -17s For more details on these failures, see this check. Results for commit 19d6268. ± Comparison against base commit 412fc5c. ♻️ This comment has been updated with latest results. |
Deferred out of this PR:
|
|
Fixed in Verified the claim before changing it rather than taking it on faith: in Worth fixing rather than deleting the reference: those comments explain why the satellite write is not ordered before the terminal status write, which is the reasoning this test exists to pin. A reader who cannot find the method loses the explanation, not just a name. |
|
|
Closing as superseded by #1075 — this branch is now empty.
So the CI run that was red here was testing main, not this contribution. On that red — it was not this diff, proven two waysThe assertion is Structurally the branch is also unreachable: it is gated on The failure itself is real and now tracked separatelyFiled as its own issue. Both kernel tests post fire-and-forget submissions back-to-back and depend on delivery order plus A hypothesis worth recording as discarded: |
Two defect fixes, and a measured "no" on parallelising
AI.Test.Does not close #1040 — see "The opt-in is backed out" below. Closes the half of #1029 that #1038
did not reach.
#1040's premise is wrong: AI.Test has no cross-class shared state
The issue attributes the parallel failures to state shared between test classes — 13 classes on one
rbuergi@systemorph.com, ~17 on theTestDatapartition — the #999 shape. The reproduction saysotherwise, and so does the structure.
MonolithMeshTestBase.ShareMeshClusterEnabledisfalse, so everyShareMeshAcrossTestsoverride in the assembly is ignored and every
[Fact]builds its own mesh. Two classes thereforenever share a workspace, a hub, a routing table, an
IStorageAdapter(TryAddSingleton, perservice-provider) or an
IMeshNodeStreamCache(likewise).TestDatais a path inside each test'sprivate in-memory store; the shared test user is a claim on each test's private
AccessService._sharedProviders, the one static collection the test base still declares, is never read or writtenwhile that kill-switch is off, and
NoStaticCollectionsTestreports zero remainingCACHE-classstatics.
The 502-call-site
TestPartitionrefactor proposed in the issue thread would have made per-classwhat is already per-test.
Defect 1 — the token-usage satellite (3 of the original 5 failures)
ThreadTokenUsageTest.WaitForUsagepoint-read{thread}/_Usage/{model}.TokenUsage.RecordUsageissubscribed as an independent side effect, deliberately NOT chained before the round's terminal
status write — its own doc comment says so — so "terminal state reached" does not imply "satellite
exists". A point read of an absent node gets an authoritative routing
NotFound, whichterminates the stream with an error; it cannot wait for a node to appear. Three tests failed
within a second:
No node found at 'TestData/_Thread/…/_Usage/usage_model'.Production never had this —
ThreadTokenChipbinds the live children query of{thread}/_Usage,which starts from the (possibly empty) collection and re-emits when the node lands.
WaitForUsagenow uses exactly that, so the test covers the read path the portal actually runs.
Negative control, verified:
UsageWatchedFromBeforeTheRound_ArrivesWhenTheSatelliteIsCreatedopens the watch before submitting, when the thread has no
_Usagenamespace at all. Against thepoint-read helper it fails in 2.6 s with the exact CI signature; against the children query it
passes.
Defect 2 — the second door into #1029, which #1038 did not close
#1038 forked deleted-vs-recycling in
NackThroughParent— deliveries the hub abandons (intakegate, disposal drain). There is a second site. A delivery can be accepted and then fault: the
handler runs while
RunLevelstill readsStarted, reaches for machineryHostedHubsCollectionhas already frozen (
SynchronizationStream's ctor →Host.GetHostedHub), and throwsHubDisposingException. That lands inMessageService's executionCatch, which still hardcodedErrorType.ShuttingDownand echoed the raw exception text — whose wording is literally "Hub …is shutting down", an
IsTransientOwnerFailuremarker. Both signals pointing the wrong way.So the consumer rode it out awaiting a reactivation that can never come, the 5 s keep-alive
heartbeat drew the authoritative NotFound from routing with nobody left to hand it to, and the
reader burned its budget reporting "unavailable" for a node provably gone.
Measured with #1038 already merged in:
MeshPluginTest.FullCrudWorkflow_CreateGetUpdateDeletestill failed 2 of 4 whole-assembly runs, identical signature and timings:
Same tombstone, same verdict. The contract sentence now lives in one place
(
DeletedAddressMessage) so the two sites cannot drift — a drift here is silent, which is preciselyhow this door stayed open.
Negative control, verified:
DeletedAddressNackClassificationTestgains both halves, forcing thedoor deterministically with a handler that throws the exception outright — no disposal race to lose.
Tombstoned ⇒
NotFound+"No node found"+ never"shutting down"; not tombstoned ⇒ShuttingDown, so the recycle path (#672) cannot be collapsed into it. Measured failing before,passing after.
Messaging.Hub.Test169/169 green on the merged tree — including main's newlylanded
DeliveryFailureClassificationWireTest.The opt-in is backed out — AI.Test needs the machine
I did opt in, on five consecutive green local runs, and CI disproved it. Three failures on
shard 4, all concurrency-stress tests — tests that deliberately saturate the machine and whose
budgets assume they have it:
PatchWorkspaceAckTest.Patch_ConcurrentUpdates_NoDeadlockCrossHubPatchAtomicityTest.ConcurrentCrossHubPatches_DoNotDropAQueuedMessageAgentChatClientDeadlockTest.GetOrderedAgentsAsync_…_DoNotDeadlockTimeoutExceptionThe middle one's own diagnostic settles it, and it is not a correctness failure:
No write failed, the owner holds all 288, and the mirror moved monotonically forward with zero
regressions — still climbing when the settle bound expired. Per the test's own printed guidance
that is "a slow box, not a lost write". Starved, not wrong.
Why no amount of local measurement could have caught this — and this PR said so before CI ran:
DOTNET_PROCESSOR_COUNT=4sizes the thread pool and GC as if there were 4 cores but does notremove the other 14. A test that spawns its own concurrency therefore still got real parallelism
locally, across 14 whole-assembly runs and five consecutive green ones; on a true 4-vCPU runner four
such classes timeshare and blow their budgets. The stated caveat predicted the failure mode; CI
demonstrated it.
This is not #1040's cross-class shared state, and not a correctness defect — it is contention
between the runner's parallelism and tests that need the machine to themselves.
The way forward is to isolate the stress classes into their own serial collection and opt in the
remainder. Choosing that set off ONE CI run would be guessing, and the only box that can measure it
is the box that cannot reproduce the starvation — so it wants its own change. Emphatically not
the alternative of widening those budgets: they are deadlock detectors, and a deadlock detector with
a padded budget detects nothing.
What the opt-in was worth, for whoever picks it up
Real CI, like for like, same branch and shard split (AI.Test is split
#1/2→shard 4,#2/2→shard 3):~1.3×, not the 1.5–2.1× the 18-core box suggested — the same compression #999 measured. Modest and
lopsided, which is itself a hint that the 599-half is serialised by its own long tests.
shard-assign.shweight left at 263 throughout.The
*_StaysLive_AndReEmitsWhen*family: ruled out, and now #1053Neither #1040's premise nor #1038 explains it: the failure on main was under serial execution;
those three tests contain no deletes at all; the shape is a 30 s timeout on a live children
query that never re-emitted after a create that completed — a missed
Addedon the change-feed →synced-query path. Filed as #1053 and fixed on main by #1060, which is merged into this branch.
What's New
Skipped. Defect 1 is test infrastructure. Defect 2 is user-visible — but it is the same statement
#1038 already shipped as
2026-08-09-deleted-node-reads-answer-not-found.md; this change is whatmakes that entry true in the accepted-then-faulted case, and a second entry repeating it would be
noise.
🤖 Generated with Claude Code