Skip to content

fix(mesh): GetMeshNode's recycling re-probe carries the read's identity - #5444

Merged
meshweaver-cloud[bot] merged 1 commit into
mainfrom
fix/getmeshnode-reprobe-keeps-identity
Sep 23, 2026
Merged

meshweaver-cloud[bot] merged 1 commit into
mainfrom
fix/getmeshnode-reprobe-keeps-identity

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

What

GetMeshNodeOutcome (src/MeshWeaver.Mesh.Contract/MeshNodeStreamExtensions.cs) now captures the caller's identity once, at Subscribe, and pins it on every GetDataRequest it posts with o.WithAccessContext(caller). Before this change only the first probe carried it.

Why: the hub=portal/reads-…, message=GetDataRequest family

Every incident in this family has the same log line: PostPipeline: AccessContext must never be null … hub=portal/reads-{meshId}, message=GetDataRequest, target=<page path> (Home, Store, AgenticEngineering, Mail, Google, Stripe, ThreeBody, Reinsurance, Doc/AI, …).

  • portal/reads-{meshId} is the mesh's shared read-issuing hub (MeshExtensions.MeshReadHub). It has PostingIdentity.User and no user of its own. GetMeshNode issues from it whenever the caller holds the root mesh hub. One such caller is the prerender read IMeshService.GetPreRenderedHtml(path), which ApplicationPage calls with the URL path as path.
  • When the owner NACKs ShuttingDown (it is recycling), GetMeshNodeOutcome sends the read again. The first probe is posted from Subscribe, where the caller's identity is set. The re-probe is posted from the NACK's OnError callback (and after the second NACK, from a pacing timer), and it read the identity from the current thread again.
  • hub.Observe restores the captured identity on OnNext only. So the OnError callback runs under whatever identity the NACK delivery carries. The Orleans router's NACK for a deactivating target grain (RoutingGrain.PostFailure) carries none. The re-probe then had no AccessContext, the never-null guard refused it and logged the error, and the read settled Unavailable. The user's page lost its pre-rendered body.

This fits the evidence: the incidents are rare, they come in bursts after rolls and package activity, and #5231 shows the same target on two pods 17 s apart. That is a recycling root read by several readers.

Repro (negative control run)

GetMeshNodeReProbeKeepsCallerIdentityTest (MeshWeaver.Data.Test) sets up a User-identity issuing hub with no standing identity, puts the caller's identity in scope only for the call, and uses an owner that NACKs the first probe ShuttingDown with no AccessContext (the way RoutingGrain.PostFailure does) and answers the second.

  • Unfixed main: FAILED. The outcome was Unavailable, only the first probe arrived, and the test host logged the production line verbatim: PostPipeline: AccessContext must never be null … hub=client/…, message=GetDataRequest, target=client/… was posted with no AccessContext (no Context, no CircuitContext).
  • With the fix: passed (total 1, succeeded 1). Both probes arrived as the caller.
  • A variant whose NACK is posted through ResponseFor (which carries the requester's identity back) passes on unfixed code. That is why the test uses the identity-less NACK the Orleans router actually sends.

No identity is invented. A caller with no identity still fails closed, exactly as before. Nothing is stamped as hub-self or System.

Docs

Doc/Architecture/AccessContextPropagation has a new anti-pattern row: a retry or re-probe that reads the identity from the current thread inside a NACK callback or timer.

Verification after deploy

These incidents log only when a page's root is recycling. After the roll, the fingerprints listed below should go quiet across the next rolls or package installs.

Pairs-with: none — no public type or member removed; one private local added inside an existing method.

Fixes #5227
Fixes #5231
Fixes #5232
Fixes #5431
Fixes #5433
Fixes #5434
Fixes #5435
Fixes #5438
Fixes #5439
Fixes #1253

(#1253's original portal/nodeops-* producer was fixed by #1279. Its reopens since 2026-09-22 are all this portal/reads-* shape, folded onto the shared fingerprint.)

🤖 Generated with Claude Code

GetMeshNodeOutcome re-issues its GetDataRequest when the owner NACKs
ShuttingDown. The first probe is posted from Subscribe, where the caller's
identity is ambient; the re-probe is posted from the NACK's OnError callback
(or a pacing timer) and re-read the ambient there. hub.Observe restores the
captured identity on OnNext only, and the Orleans router's NACK for a
deactivating grain (RoutingGrain.PostFailure) carries no AccessContext, so on
portal/reads-{meshId} (a User hub with no user) the re-probe was failed closed
by the never-null guard and the read settled Unavailable.

Capture the identity once at Subscribe and pin it on every probe with
WithAccessContext. A caller with no identity still fails closed as before.

Fixes #5227, #5231, #5232, #5431, #5433, #5434, #5435, #5438, #5439.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 23, 2026 04:37
@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

  1 files  ±0    1 suites  ±0   2m 44s ⏱️ -8s
326 tests ±0  326 ✅ ±0  0 💤 ±0  0 ❌ ±0 
330 runs  ±0  330 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit e17dc51. ± Comparison against base commit 39f4949.

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.

Copilot review overview

🔵 Needs a closer look

Use required-service resolution for AccessService before approval.

Review effort: Lite
Findings: None

What changed in this PR

Preserves caller access identity across GetMeshNode recycling re-probes.

Changes:

  • Pins caller identity on every read probe.
  • Adds regression coverage for identity-less shutdown NACKs.
  • Documents the retry identity-propagation pattern.
File Summary
test/​MeshWeaver.Data.Test/​GetMeshNodeReProbeKeepsCallerIdentityTest.cs Adds regression coverage for retry identity propagation.
src/​MeshWeaver.Mesh.Contract/​MeshNodeStreamExtensions.cs Captures and applies caller identity; moderate finding: resolve AccessService as a required dependency.
src/​MeshWeaver.Documentation/​Data/​Architecture/​AccessContextPropagation.md Documents the retry/re-probe identity anti-pattern.

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

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

543 tests   - 1 080   543 ✅  - 1 080   1m 14s ⏱️ - 2m 15s
  1 suites  -     1     0 💤 ±    0 
  1 files    -     1     0 ❌ ±    0 

Results for commit e17dc51. ± Comparison against base commit 39f4949.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

429 tests   - 14   429 ✅  - 14   1m 10s ⏱️ +16s
  2 suites  -  1     0 💤 ± 0 
  2 files    -  1     0 ❌ ± 0 

Results for commit e17dc51. ± Comparison against base commit 39f4949.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

    2 files   -   1      2 suites   - 1   6m 21s ⏱️ +3s
2 589 tests +601  2 589 ✅ +601  0 💤 ±0  0 ❌ ±0 
2 590 runs  +601  2 590 ✅ +601  0 💤 ±0  0 ❌ ±0 

Results for commit e17dc51. ± Comparison against base commit 39f4949.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

1 585 tests  +854   1 585 ✅ +1 046   9m 14s ⏱️ + 2m 9s
    2 suites  -   1       0 💤  -   192 
    2 files    -   1       0 ❌ ±    0 

Results for commit e17dc51. ± Comparison against base commit 39f4949.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    4 files   -   1      4 suites   - 1   15m 9s ⏱️ +17s
3 349 tests  - 643  3 349 ✅  - 641  0 💤  - 2  0 ❌ ±0 
3 353 runs   - 643  3 353 ✅  - 641  0 💤  - 2  0 ❌ ±0 

Results for commit e17dc51. ± Comparison against base commit 39f4949.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

   12 files   -   5     12 suites   - 5   35m 54s ⏱️ +22s
8 821 tests  - 282  8 821 ✅  - 88  0 💤  - 194  0 ❌ ±0 
8 830 runs   - 282  8 830 ✅  - 88  0 💤  - 194  0 ❌ ±0 

Results for commit e17dc51. ± Comparison against base commit 39f4949.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants