Skip to content

test(orleans): wait for the pod-hub CLAIM, not for reachability (#3298) - #3313

Merged
meshweaver-cloud[bot] merged 1 commit into
mainfrom
fix/3298-crosssilo-nack-teardown-race
Sep 4, 2026
Merged

meshweaver-cloud[bot] merged 1 commit into
mainfrom
fix/3298-crosssilo-nack-teardown-race

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Closes #3298.

The test asserts a NACK arriving over the directed pod-hub transport after erasing the sender's
stream subscription. It never checked the precondition that makes that transport available.

The mechanism

RegisterStream establishes two things, on different clocks:

Established Used by
the local route synchronously, never fails in-process delivery
the pod-hub claim asynchronously, capped backoff (100 ms → 2 s), no give-up on a silo every directed cross-silo call, including PostFailure's NACK

Until the claim lands, [PreferLocalPlacement] puts a directed IPodHubGrain call on the caller's
silo — which has no local route for the address — and it answers PodHubNotHere. PostFailure then
falls back to the stream, which this test has just emptied, and the NACK is discarded in silence.
That is the reported failure exactly: a 30 s timeout on the final wait, with both controls green.

Why the existing probe could not catch it

It proved reachability, and at that moment reachability is satisfied by the stream — the very
transport the test erases three lines later. So it was green whether or not the claim had landed.

It is also adversarial to the claim it appeared to prove: it posts from the other silo in a loop,
and each failed directed call mints a throw-away activation there that the owner's next Attach must
bounce off, restarting the backoff. That is why this reproduced by luck rather than by tree — the
issue's own control arm (two heads differing only in a string-assertion test in another assembly).

Measured, not inferred

With one bounce forced and the claim backoff pinned long, the old probe goes green in under a
second
while PodHubClaimSettled has demonstrably not completed — 6 runs, 6 times.

I deliberately did not commit that experiment. Its setup depends on an activation-lifetime
ordering I cannot enforce (whether the throw-away activation is still alive when Attach arrives),
so as a permanent test it would risk becoming the next flake — the exact thing this PR is fixing.
It belongs in the record, not in the suite.

The fix

Wait on PodHubClaimSettled — the positive signal for "the claim stopped attempting".
OrleansCrossSiloReplyTest already waits on it before its own directed cross-silo delivery, with the
comment "Claimed ⇒ a cross-silo directed delivery lands"; this brings the two into line. A
settle-by-silence poll is explicitly not an acceptable substitute (see the seam's remarks: the retry
hops the thread-pool scheduler, so a poll reads mid-hop).

SiloHostedHub_ReceivesDelivery_EvenWithItsStreamSubscriptionGone carries the same latent gap —
it waits for the stream subscription, a different condition that merely happens to take similar
time — so it gets the same wait. Two tests, one cause.

No production code changes. Falling back and then saying so loudly is what the platform is
designed to do during the claim window; the defect was in what the test asserted without checking.

What this rules out

Per the issue's own history, and re-checked here rather than assumed:

Verification

-c Release -warnaserror, 0 Error(s) on MeshWeaver.Hosting.Orleans.Test and
MeshWeaver.Documentation.Test. Tests: 20 passed across PodHubTransportTest,
PodHubClaim* and OrleansCrossSiloReplyTest; DocumentationLinkIntegrityTest passed.

The rule is written up in Doc/Architecture/OrleansTestRoutingPattern → "Reachability is not a
claim"
, so the next test that isolates one transport to assert another finds it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RpJi9EZHtGYQCahUwWa7jH

CrossSiloNack_ReachesASenderWhoseStreamSubscriptionIsGone erases the sender's
stream subscription and then asserts that a NACK arrives over the DIRECTED
pod-hub transport. That assertion has a precondition the test never checked: the
claim which makes the directed transport available at all.

RegisterStream establishes two things on different clocks. The local route is
synchronous and never fails. The cluster-wide pod-hub claim is asynchronous,
retried on a capped backoff (100ms doubling to 2s) with NO give-up on a silo.
Until it lands, [PreferLocalPlacement] puts a directed IPodHubGrain call on the
CALLER's silo, which has no local route for the address, and it answers
PodHubNotHere. PostFailure then falls back to the stream — which this test has
just emptied — and the NACK is discarded in silence. That is the reported
failure exactly: a 30s timeout on the final wait with both controls green.

The probe that was supposed to cover this proved only REACHABILITY, and at that
moment reachability is satisfied by the STREAM — the transport the test is about
to erase. So it was green whether or not the claim had landed. Worse, it is
adversarial to the claim: it posts from the other silo in a loop, and each failed
directed call mints a throw-away activation there that the owner's next Attach
must bounce off, restarting the backoff. That is why this reproduced by luck
rather than by tree.

MEASURED, not inferred. With one bounce forced and the claim backoff pinned long,
the old probe goes green in under a second while PodHubClaimSettled has
demonstrably not completed — 6 runs, 6 times. I did not commit that experiment:
its setup depends on an activation-lifetime ordering I cannot ENFORCE, so as a
permanent test it would risk becoming the next flake. It is recorded in the PR
and on the issue instead.

The fix waits on PodHubClaimSettled — the positive signal for "the claim stopped
attempting", which OrleansCrossSiloReplyTest already waits on before its own
directed cross-silo delivery. Its remarks say why a settle-by-silence poll is not
an acceptable substitute, so this uses the signal rather than a poll.

SiloHostedHub_ReceivesDelivery_EvenWithItsStreamSubscriptionGone has the same
latent gap — it waits for the stream SUBSCRIPTION, a different condition that
merely happens to take similar time — so it gets the same wait.

No production code changes: the platform behaviour here (fall back, then say so
loudly) is what it is designed to do during the claim window.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RpJi9EZHtGYQCahUwWa7jH
Copilot AI lite review requested due to automatic review settings September 4, 2026 21:12
@meshweaver-cloud
meshweaver-cloud Bot enabled auto-merge September 4, 2026 21:12

Copilot AI 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.

🟢 Approval recommended

The change aligns the tests with the existing PodHubClaimSettled seam and adds documentation; only a small comment/backoff-value mismatch needs a minor tweak.

Pull request overview

This PR stabilizes Orleans pod-hub transport tests by waiting for the pod-hub claim to settle (the prerequisite for directed cross-silo delivery) instead of inferring readiness from reachability, and documents the pattern to prevent similar flakes.

Changes:

  • Updated two PodHubTransportTest cases to await PodHubClaimSettled before isolating transports.
  • Added a ClaimSettled helper to centralize the “claim is ready” wait logic.
  • Documented the rule (“Reachability is not a claim”) in the Orleans testing routing pattern guide.
File summaries
File Description
test/MeshWeaver.Hosting.Orleans.Test/PodHubTransportTest.cs Adds an explicit wait for claim settlement before removing stream subscriptions and asserting directed delivery.
src/MeshWeaver.Documentation/Data/Architecture/OrleansTestRoutingPattern.md Documents the testing rule and provides a canonical code pattern for waiting on claim settlement.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

// now subscriber-less stream, and the NACK was silently discarded — a 30 s timeout on the
// final wait with both controls having passed, which is the failure exactly as reported.
//
// The claim retries on a capped backoff (250 ms doubling to 4 s) with no give-up on a silo,
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

251 tests   251 ✅  2m 16s ⏱️
  1 suites    0 💤
  1 files      0 ❌

Results for commit a69176a.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

475 tests   475 ✅  42s ⏱️
  1 suites    0 💤
  1 files      0 ❌

Results for commit a69176a.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

368 tests   366 ✅  55s ⏱️
  3 suites    2 💤
  3 files      0 ❌

Results for commit a69176a.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    4 files      4 suites   1m 44s ⏱️
1 487 tests 1 487 ✅ 0 💤 0 ❌
1 488 runs  1 488 ✅ 0 💤 0 ❌

Results for commit a69176a.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

2 291 tests   2 291 ✅  2m 41s ⏱️
    3 suites      0 💤
    3 files        0 ❌

Results for commit a69176a.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

588 tests   396 ✅  2m 57s ⏱️
  3 suites  192 💤
  3 files      0 ❌

Results for commit a69176a.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Test Results

   15 files     15 suites   11m 17s ⏱️
5 460 tests 5 266 ✅ 194 💤 0 ❌
5 461 runs  5 267 ✅ 194 💤 0 ❌

Results for commit a69176a.

@meshweaver-cloud
meshweaver-cloud Bot added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit 42f0cbc Sep 4, 2026
28 checks passed
@rbuergi
rbuergi deleted the fix/3298-crosssilo-nack-teardown-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.

Flake: PodHubTransportTest.CrossSiloNack_ReachesASenderWhoseStreamSubscriptionIsGone — red on shard 0 with a control arm that isolates it

2 participants