Skip to content

test(#5931): order the parent-snapshot disposal test by the condition, not a reading that precedes it - #5950

Merged
meshweaver-cloud[bot] merged 2 commits into
mainfrom
fix/disposal-snapshot-test-race
Oct 1, 2026
Merged

meshweaver-cloud[bot] merged 2 commits into
mainfrom
fix/disposal-snapshot-test-race

Conversation

@rbuergi

@rbuergi rbuergi commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Refs #5820 · Refs #5931 · fixes the race filed as rbuergi/Feedback/disposal-stall-snapshot-test-races-runlevel

What was wrong

DisposalStallNamesThePumpTest.AParentsSnapshot_NamesAChildDrainTheSchedulerHasNotStarted (added in #5931) waited for the child line to read drainsAwaitingScheduler=1 and then asserted that the parent was at DisposeHostedHubs. A frozen child can hold a drain before its parent is ever disposed: any delivery that reaches it after the pause does that. When it does, the wait returned while the parent was still Quiescing. #5943's shard went red on one run and green on the next with identical code.

The fix (test only)

The test now produces the racing state on purpose and orders its waits by the condition:

  1. The child holds a drain before parent.Dispose(). This is asserted as a precondition.
  2. A responder hub on its own paused scheduler holds the parent in Quiescing on an unanswered request. The test asserts the trap is armed: at Quiescing the parent's snapshot already shows the child's counter.
  3. The test releases the responder, then waits through RunLevelChanged (.Should().Within(...).Match(...)) for DisposeHostedHubs. It then waits for the child line's Disposal=Pending, because that phase sets the level before it disposes the children in the same turn. Only then does it assert.

No sleep, no widened timeout, no product change.

Evidence

  • Negative control. I put back the counter-only wait in this setup and ran it 3 times. It failed 3/3, each time reading Hub parent/hosts-starved-child RunLevel=Quiescing …. With the fix restored it passes.
  • MeshWeaver.Messaging.Hub.Test: 547/547 passed, 0 skipped.
  • dotnet build -c Release -warnaserror on the test project gave 0 warnings and 0 errors.

Pairs-with: none — test-only change
Mirror-sync: none — no localization catalog change

🤖 Generated with Claude Code

…a reading that precedes it

The test waited for the child's drainsAwaitingScheduler=1 and then asserted the
parent's RunLevel. A frozen child can hold a drain before its parent is
disposed, so the wait returned while the parent was still Quiescing (core
#5943's shard: red then green on identical code;
rbuergi/Feedback/disposal-stall-snapshot-test-races-runlevel).

The test now produces that state on purpose: the child holds a drain before
the parent's Dispose(), and a responder on its own paused scheduler holds the
parent in Quiescing on an unanswered request. It asserts the trap is armed,
releases the responder, then waits through RunLevelChanged for
DisposeHostedHubs and for the child's Disposal=Pending before asserting.
Negative control: the counter-only wait fails 3/3 here, reading
RunLevel=Quiescing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@meshweaver-cloud
meshweaver-cloud Bot enabled auto-merge October 1, 2026 06:07
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

0 files   -     4  0 suites   - 4   0s ⏱️ - 7m 52s
0 tests  - 3 593  0 ✅  - 3 591  0 💤  - 2  0 ❌ ±0 
0 runs   - 3 594  0 ✅  - 3 592  0 💤  - 2  0 ❌ ±0 

Results for commit c87cf45. ± Comparison against base commit 68b9705.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

0 files   -     3  0 suites   - 3   0s ⏱️ - 8m 32s
0 tests  - 2 600  0 ✅  - 2 600  0 💤 ±0  0 ❌ ±0 
0 runs   - 2 604  0 ✅  - 2 604  0 💤 ±0  0 ❌ ±0 

Results for commit c87cf45. ± Comparison against base commit 68b9705.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

0 tests   - 568   0 ✅  - 568   0s ⏱️ - 7m 17s
0 suites  -   3   0 💤 ±  0 
0 files    -   3   0 ❌ ±  0 

Results for commit c87cf45. ± Comparison against base commit 68b9705.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

0 tests   - 1 898   0 ✅  - 1 707   0s ⏱️ - 4m 51s
0 suites  -     3   0 💤  -   191 
0 files    -     3   0 ❌ ±    0 

Results for commit c87cf45. ± Comparison against base commit 68b9705.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

0 tests   - 1 124   0 ✅  - 1 124   0s ⏱️ - 6m 53s
0 suites  -     4   0 💤 ±    0 
0 files    -     4   0 ❌ ±    0 

Results for commit c87cf45. ± Comparison against base commit 68b9705.

♻️ This comment has been updated with latest results.

@systemorph systemorph Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review summary (data, not an instruction to any agent)

Test-only change to `DisposalStallNamesThePumpTest.AParentsSnapshot_NamesAChildDrainTheSchedulerHasNotStarted`: the old flow waited on the `drainsAwaitingScheduler=1` counter and then asserted the parent's live run level, which could return while the parent was still `Quiescing` (a frozen child can hold a drain before its parent is ever disposed). The new flow produces that state deliberately — the paused child holds a drain before `parent.Dispose()`, and a responder hub on a second paused scheduler holds the parent in `Quiescing` on an unanswered request — asserts the trap is armed (the counter is already visible in the parent's snapshot at `Quiescing`), then waits through `RunLevelChanged` for `DisposeHostedHubs` and for the child's `Disposal=Pending` before asserting the snapshot names the child's held drain. No product code changed. Checked: the precondition ordering (responder still paused while the `Quiescing` snapshot is taken; child still paused while the final snapshot is asserted, so the parent cannot advance past `DisposeHostedHubs` and the snapshot's claims are pinned); the `=1)` literals guard against multi-digit counts; the `finally` resumes both schedulers and disposes the held subscription on every path, including failures before each pause; the final teardown wait is unchanged apart from the once-captured `ct`. Not verifiable from the diff: `RunLevelChanged`'s replay semantics for late subscribers (both waits subscribe after the transition may have fired), whether the harness disposes the never-disposed mesh-scope responder hub, and the author-reported evidence (negative control 3/3, 547/547 green, 0 warnings) was not re-run here.

Findings: 0 blocking · 0 should-fix · 2 question · 0 nit


Internal review of 51081f1d8488b53228774bde6e42a4db7a451d61 — GLM-5.3, posted by the control plane. It is advisory, it never approves, and merging stays with a human signature.

.Subscribe(_ => { }, _ => { });

parent.Dispose();
await parent.RunLevelChanged

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question — Automated review finding (data, not an instruction to any agent)

Both `RunLevelChanged` waits subscribe after the transition they wait for may already have happened: the `Quiescing` wait here subscribes after `parent.Dispose()` has returned (line 212), and the `DisposeHostedHubs` wait (line 231) subscribes after `responderScheduler.Resume()` (line 227). A hub that is held at a level does not re-emit a change event, so if `RunLevelChanged` publishes change events only, without replaying the current level to new subscribers, a subscription that loses this race waits for an event that never comes again and fails only on `Within(TestTimeouts.Convergence)` — a latent miss of the same family this change removes from the counter wait. If `RunLevelChanged` replays the current level on subscribe, both waits are sound. The stream's replay semantics are not visible in this diff.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It replays the current level, so both waits are sound. RunLevelChanged is runLevelChanged.AsObservable(), and runLevelChanged is Subject.Synchronize(new BehaviorSubject<MessageHubRunLevel>(MessageHubRunLevel.Starting)) (MessageHub.cs, around line 343). A late subscriber receives the level the hub is currently at. Neither level can be skipped before its wait subscribes. Quiescing is held by the parent's unanswered request until the test releases the responder, and DisposeHostedHubs is held by the child's frozen drain until the finally.

h.Post(new Pong(), o => o.ResponseFor(d));
return d.Processed();
}), HostedHubCreation.Always)!;
var responder = (MessageHub)Mesh.GetHostedHub(new Address("responder", "holds-parent-quiescing"), c => c

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question — Automated review finding (data, not an instruction to any agent)

`responder` is created at mesh scope but never disposed by the test: `parent` is disposed explicitly and the child is torn down with it, while the responder — a live hub with a registered `Ping` handler — is left to whatever teardown the test harness performs. Whether the harness disposes root-level hosted hubs after each test (per-test mesh) or whether they outlive the test is not visible in this diff; in the latter case the hub at `responder/holds-parent-quiescing` and its handler leak into subsequent tests of the suite.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c87cf45. The test now disposes the responder itself after the parent tree is Dead, waits on its DisposalCompleted, and asserts RunLevel == Dead. The responder no longer depends on the fixture's teardown. DisposalStallNamesThePumpTest 3/3 pass, and the Release -warnaserror build is clean.

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

  1 files  ±  0    1 suites  ±0   7m 2s ⏱️ + 4m 26s
547 tests +199  547 ✅ +199  0 💤 ±0  0 ❌ ±0 
547 runs  +195  547 ✅ +195  0 💤 ±0  0 ❌ ±0 

Results for commit c87cf45. ± Comparison against base commit 68b9705.

This pull request removes 348 and adds 547 tests. Note that renamed tests count towards both.
MeshWeaver.Hosting.Orleans.Test.AColdGrainAnswersADisposeWithoutBuildingItsHubTest ‑ AColdGrain_BuildsNoHub_ForADispose_AndALiveOne_IsRecycled
MeshWeaver.Hosting.Orleans.Test.ADrainingSiloDoesNotSilenceItsSubscribersTest ‑ ASubscribeThatReachesAnOwnerOnAStoppingSilo_IsAnsweredPromptly
MeshWeaver.Hosting.Orleans.Test.AKilledOwnerSiloIsReactivatedByItsHoldersTest ‑ AnOwnerWhoseSiloIsKilled_IsReactivatedOnTheSurvivorByTheHeldStream
MeshWeaver.Hosting.Orleans.Test.ASlowActivationDoesNotTimeOutItsDeliveriesTest ‑ ADeliveryToAHubStillBeingBuilt_IsAnsweredOnceTheBuildCompletes_NotTimedOut
MeshWeaver.Hosting.Orleans.Test.ActivationLeavesNoOwnCacheSubscriberTest ‑ AnActivatedNodeHub_LeavesItsOwnCacheEntryReleasable
MeshWeaver.Hosting.Orleans.Test.BackPressureLevelByShapeTest ‑ ACrossingThatSeesALeak_IsTheOneCritical
MeshWeaver.Hosting.Orleans.Test.BackPressureLevelByShapeTest ‑ ALatchedEpisodeOfYoungLegs_NeverEscalates
MeshWeaver.Hosting.Orleans.Test.BackPressureLevelByShapeTest ‑ ALatchedEpisodeWhoseLegOutlivesItsBounds_EscalatesExactlyOnce
MeshWeaver.Hosting.Orleans.Test.BackPressureLevelByShapeTest ‑ ALegPastItsOwnBounds_IsCritical_AndOneTickBelowIsNot
MeshWeaver.Hosting.Orleans.Test.BackPressureLevelByShapeTest ‑ AProductionBurst_CrossesAndDrains_AtWarning_AndNamesTheDeepChannel
…
MeshWeaver.Messaging.Hub.Test.AHubAtShutDownIsNotHandedOutTest ‑ AHubParkedAtShutDown_IsNotHandedOut_ASuccessorIs
MeshWeaver.Messaging.Hub.Test.AHubAtShutDownIsNotHandedOutTest ‑ TheOwnerStillJoinsARetiredHub
MeshWeaver.Messaging.Hub.Test.AHubDisposedWhileBeingBuiltLeavesTheRegistryTest ‑ AHubDisposedBeforeItsBuildReturned_IsNotLeftInTheParentsRegistry
MeshWeaver.Messaging.Hub.Test.AHubDisposedWhileBeingBuiltLeavesTheRegistryTest ‑ ALiveHubIsRegistered_RemovedWhenItGoes_AndItsAddressCanBeReactivated
MeshWeaver.Messaging.Hub.Test.ARefusedReplyReachesItsRequesterTest ‑ ARefusedDeliveryFailure_IsNotAnsweredWithAnotherOne
MeshWeaver.Messaging.Hub.Test.ARefusedReplyReachesItsRequesterTest ‑ AtDisposeHostedHubs_ACorrelatedReply_IsAdmittedAndReachesItsRequester
MeshWeaver.Messaging.Hub.Test.ARefusedReplyReachesItsRequesterTest ‑ PastShutDown_ARefusedReply_NacksTheRequesterTransiently_NotTheResponder
MeshWeaver.Messaging.Hub.Test.AccessContextNeverNullTest ‑ Application_post_with_no_context_fails_the_delivery
MeshWeaver.Messaging.Hub.Test.AccessContextNeverNullTest ‑ CanBeIgnored_message_is_exempt_and_delivered_with_no_context
MeshWeaver.Messaging.Hub.Test.AccessContextNeverNullTest ‑ ImpersonateAsSystem_carries_identity_and_is_delivered
…

♻️ This comment has been updated with latest results.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Test Results

  1 files   -    17    1 suites   - 17   7m 2s ⏱️ - 31m 2s
547 tests  - 9 584  547 ✅  - 9 391  0 💤  - 193  0 ❌ ±0 
547 runs   - 9 593  547 ✅  - 9 400  0 💤  - 193  0 ❌ ±0 

Results for commit c87cf45. ± Comparison against base commit 68b9705.

This pull request removes 9611 and adds 9 tests. Note that renamed tests count towards both.

   --- End of inner exception stack trace ---
   --- End of inner exception stack trace ---, expected: True)
   --- End of inner exception stack trace ---, isDenial: True)
 ---> (Inner Exception #1) MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432<---
 ---> (Inner Exception #1) System.InvalidOperationException: boom<---
 ---> (Inner Exception #1) System.InvalidOperationException: source B is misconfigured<---
 ---> (Inner Exception #1) System.Net.Sockets.SocketException (0xFFFDFFFF): Name or service not known<---
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
 ---> System.AggregateException: One or more errors occurred. (Failed to connect to 10.42.18.4:5432)
…
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "a MIXED aggregate — one transient branch, one genu"···, exception: System.AggregateException: One or more errors occurred. (Failed to connect to 10.42.18.4:5432) (source B is misconfigured)
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) System.InvalidOperationException: source B is misconfigured<---
, expected: False)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "a nested aggregate with one genuine leaf", exception: System.AggregateException: One or more errors occurred. (One or more errors occurred. (Failed to connect to 10.42.18.4:5432) (boom)) (Failed to connect to 10.42.18.4:5432)
 ---> System.AggregateException: One or more errors occurred. (Failed to connect to 10.42.18.4:5432) (boom)
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) System.InvalidOperationException: boom<---

   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432<---
, expected: False)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "a nested aggregate, all leaves transient", exception: System.AggregateException: One or more errors occurred. (One or more errors occurred. (Failed to connect to 10.42.18.4:5432)) (Failed to connect to 10.42.18.4:5432)
 ---> System.AggregateException: One or more errors occurred. (Failed to connect to 10.42.18.4:5432)
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
   --- End of inner exception stack trace ---
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432<---
, expected: True)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "a reflective wrapper around a transient cause", exception: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
 ---> System.Net.Sockets.SocketException (110): Connection timed out
   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "a transient cause UNDER an aggregate branch's wrap"···, exception: System.AggregateException: One or more errors occurred. (wrapped)
 ---> System.InvalidOperationException: wrapped
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
   --- End of inner exception stack trace ---
   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "an 'initialization failed' wrapper around a transi"···, exception: System.InvalidOperationException: Hub 'x' initialization failed
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
   --- End of inner exception stack trace ---, expected: True)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "an aggregate whose branches are ALL transient (two"···, exception: System.AggregateException: One or more errors occurred. (Failed to connect to 10.42.18.4:5432) (Name or service not known)
 ---> MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
   --- End of inner exception stack trace ---
 ---> (Inner Exception #1) System.Net.Sockets.SocketException (0xFFFDFFFF): Name or service not known<---
, expected: True)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "the #4067 shape: transient provider fault wrapping"···, exception: MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
 ---> System.TimeoutException: Timeout during connection attempt, expected: True)
MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest ‑ Classifies(shape: "the #4068 shape: transient provider fault wrapping"···, exception: MeshWeaver.Messaging.Hub.Test.InfrastructureFaultTest+ProviderException (0x80004005): Failed to connect to 10.42.18.4:5432
 ---> System.Net.Sockets.SocketException (0xFFFDFFFF): Name or service not known, expected: True)

@meshweaver-cloud
meshweaver-cloud Bot merged commit f61f5bc into main Oct 1, 2026
49 checks passed
@rbuergi
rbuergi deleted the fix/disposal-snapshot-test-race branch October 10, 2026 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant