Skip to content

Grain activation no longer times out waiting on itself - #1062

Merged
rbuergi merged 4 commits into
mainfrom
fix/cache-subscribe-faults-activation
Aug 10, 2026
Merged

rbuergi merged 4 commits into
mainfrom
fix/cache-subscribe-faults-activation

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

The defect

On memex.systemorph.com (AKS ns memex) Orleans grain activations faulted and retried forever:

fail: MeshWeaver.Hosting.Orleans.MessageHubGrain[0]
      [ACTIVATE] Grain Edu: activation faulted for Edu
System.TimeoutException: No response received in hub cache/tYwK_RNWZUSCQGvEPNvvzg within 00:01:00
for request SubscribeRequest (id=...) → target Edu.

The error names the activating grain as an unreachable target — because the request was the grain's own.

Root cause

MessageHubGrain.OnActivateAsync merged streamCache.GetStream(addressPath) — the grain's OWN
address — into the activation source. The cache opens that upstream with
GetMeshNodeStreamBypassCache(path), i.e. a SubscribeRequest addressed at path, which the
RoutingGrain delivers straight back to this grain, where DeliverMessage parks it on
_hubReadyRaw until CompleteActivation runs.

So on a cold entry the branch is a strict self-loop: it cannot answer until the very activation
it feeds has finished. Its only possible terminal is "the grain did not answer itself" — no
information about the node. Observable.Merge forwarded that terminal anyway, so the cache hub's
60 s RequestTimeout became a hard ceiling on TOTAL activation.

That ceiling pre-empted both deliberate slow-path designs downstream, 100% of the time:

  • NodeTypeEnrichmentHelpers.WaitForCompileSettled disarms its wall clock while a compile is in
    flight — a cold pod compiling many dynamic types is meant to run past the budget.
  • BuildEnrichmentChain catches a stuck type into a visible compilation-error overlay so the hub
    activates anyway.

Neither could be reached. Orleans retried; the cache's transient-streak breaker then replayed the
cached TimeoutException into each fresh activation, so the retries faulted in milliseconds
(measured burst at 03:57Z: +0 s/+1 s/+2 s/+3 s). The Edu partition was unreadable to users and to
MCP with the identical exception.

Why only this portal (same image on all three)

Measured over 30 h of Loki (ns memex): ~36 activation faults an hour, and every faulting grain
was a nodeType:Store/Plugin node
— Edu (231), Feedback, Skill, Essentials, Publish,
Collaboration, DataModelling, Agent, Instances. Exactly the nodeType:Store/Plugin set.

  • memex.systemorph.com: Store/Plugin is installed as a dynamic in-mesh NodeType sitting at
    CompilationStatus = Error — CS0117: 'AiSettingsNodeType' does not contain a definition for 'AddSkillSource' (that method was deleted in AiSettingsNodeType.AddSkillSource is fire-and-forget — a fast uninstall can be undone by a late install write #683; the in-mesh plugin source still calls it, and
    no build or test can see it). That is precisely the case where enrichment legitimately runs long.
  • memex.meshweaver.cloud (control): Store/Plugin does not exist at all, so enrichment's 3 s
    existence probe answers immediately and Take(1) disposes the self-loop long before 60 s.

Data-dependent, not version-dependent — consistent with all three portals running
3.0.0-rc1.ci.2509. The Monolith routing path has no equivalent self-loop, which is why no existing
test caught it.

The fix

The cache branch may still supply a VALUE (a warm entry lets a reactivation skip the storage
read) but never a TERMINAL. ComposeActivationSource collapses a faulted self-referential branch
to Empty and logs the exception in full — it is not swallowed.

Activation keeps a complete set of terminal outcomes without it: the path resolver's node drives
enrichment; nothing within FirstNodeResolutionTimeout throws the precise "no MeshNode emitted / no
query provider claims its partition" error; both branches finishing with no node completes the
source (Empty, not Never, is what keeps the missing-node answer prompt); enrichment faults
surface as the overlay.

ComposeActivationSource / BuildActivationChain are extracted as internal static so the whole
chain runs in virtual time — same seam as WaitForCompileSettled, same precedent as
LongRunningOperationCapExceeded.

Verification

MessageHubGrainActivationSourceTest — 7 tests, virtual time, 40 ms. Reverting only the composition
to plain Observable.Merge fails 3 of them with the prod signature verbatim:

Activation faulted, but no branch of it had anything to fault ON.
TimeoutException: No response received in hub cache/tYwK_RNWZUSCQGvEPNvvzg within 00:01:00
for request SubscribeRequest (id=...) → target Edu.
  • dotnet build -c Release -warnaserror — MeshWeaver.Hosting.Orleans + its test project: 0 warnings, 0 errors.
  • 20 grain tests green under Release/-warnaserror, including the 9 Orleans cluster tests that
    guard the semantics touched here — GrainActivationCompletesFastTest.NonExistentPath_ActivationFailsFast_NotThirtySecondTimeout,
    OrleansBrokenNodeTypeAccessTest (×2), OrleansIdleReactivationNoWedgeTest (×2),
    OrleansUnresolvableNodeNoWedgeTest (×3), PartitionRootActivationTest.
  • WhatsNewEntryIntegrityTest + DocumentationLinkIntegrityTest green.

Follow-up (separate, not in this PR)

The aggravator is a data defect in the plugins repo: Store/Plugin's in-mesh source calls
AiSettingsNodeType.AddSkillSource, deleted from the framework in #683. It needs porting to
AiSourcesInstallHook. This PR makes the platform survive it (the type shows its compilation-error
overlay instead of wedging every plugin partition), but the type still needs to compile.

🤖 Generated with Claude Code

MessageHubGrain.OnActivateAsync merged `streamCache.GetStream(addressPath)` —
the grain's OWN address — into the activation source. The cache opens that
upstream with GetMeshNodeStreamBypassCache(path), i.e. a SubscribeRequest
addressed at `path`, which the RoutingGrain delivers straight back to THIS
grain, where DeliverMessage parks it on _hubReadyRaw until CompleteActivation
runs. On a cold entry the branch is a strict self-loop: it cannot answer until
the very activation it feeds has finished, so its only possible terminal is
"the grain did not answer itself".

Observable.Merge forwarded that terminal, so the cache hub's 60s RequestTimeout
became a hard ceiling on TOTAL activation and surfaced as

  [ACTIVATE] Grain Edu: activation faulted for Edu
  TimeoutException: No response received in hub cache/<id> within 00:01:00
  for request SubscribeRequest (id=...) -> target Edu.

naming the activating grain as an unreachable target. The ceiling pre-empted
both deliberate slow-path designs downstream: WaitForCompileSettled DISARMS its
wall clock while a compile is in flight, and BuildEnrichmentChain catches a
stuck type into a visible compilation-error overlay so the hub activates anyway.
Neither could be reached. Orleans retried, the cache's transient-streak breaker
replayed the cached timeout into each fresh activation, and the retries faulted
in milliseconds.

Measured on memex.systemorph.com (ns memex) 2026-08-09/10: ~36 activation
faults an hour, and every faulting grain was a nodeType:Store/Plugin node
(Edu, Agent, Collaboration, DataModelling, Essentials, Feedback, Instances,
Publish, Skill). Same image on all three portals; only this one has
Store/Plugin installed as a dynamic NodeType sitting at CompilationStatus=Error
(CS0117 AiSettingsNodeType.AddSkillSource, deleted in #683), which is exactly
the case where enrichment legitimately runs long. Data-dependent, not version-
dependent. The Edu partition was unreadable to users and to MCP with the same
exception.

The cache branch may still supply a VALUE (a warm entry lets a reactivation
skip the storage read) but no longer a TERMINAL: it is collapsed to Empty and
the exception is logged in full. Activation keeps a complete set of terminal
outcomes without it — the path resolver's node, the 30s first-emission
TimeoutException, the "no usable node" completion (prompt, which is why Empty
and not Never), and enrichment's overlay.

ComposeActivationSource / BuildActivationChain are extracted as internal statics
so MessageHubGrainActivationSourceTest drives the whole chain in virtual time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 10, 2026 05:15

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.

Pull request overview

Fixes an Orleans grain activation deadlock/time-budget failure where a self-referential mesh-node-cache subscribe could time out during activation and incorrectly fault the activation (causing infinite retries and unrecoverable plugin partitions).

Changes:

  • Neutralize terminal faults from the self-referential “own address” cache branch during MessageHubGrain activation while still allowing it to contribute a warm value.
  • Extract activation-source composition and activation-chain building into testable internal static helpers, including virtual-time control of the first-node budget.
  • Add a focused virtual-time test suite to pin the regression and document the production signature; add a WhatsNew entry describing the user-visible impact/fix.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
test/MeshWeaver.Hosting.Orleans.Test/MessageHubGrainActivationSourceTest.cs Adds virtual-time tests that reproduce and guard the activation self-loop timeout behavior.
src/MeshWeaver.Hosting.Orleans/MessageHubGrain.cs Changes activation source composition to ignore self-cache terminal faults and factors activation chain into reusable helpers.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-10-plugin-pages-stop-timing-out.md Adds a user-facing “What’s New” entry for the fix.

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

Comment on lines +330 to +335
/// <para><b>The branches.</b> <paramref name="pathResolverStream"/> is authoritative: it reads
/// the node straight out of storage, in-process, with no hub round-trip.
/// <paramref name="ownAddressCacheStream"/> is an ACCELERATOR: when the process-wide
/// <c>IMeshNodeStreamCache</c> already holds a hydrated entry for this path (a reader kept it
/// warm across an idle-collection), it replays the node instantly and activation skips the
/// storage read.</para>
/// compilation-error OVERLAY so the hub activates anyway. Neither could ever be reached: the
/// self-loop faulted the activation first, 100% of the time. Orleans then retried, the cache's
/// transient-streak breaker replayed the cached timeout into each fresh activation, and the
/// retries faulted in milliseconds — the node stayed unreadable to users and to MCP.</para></para>
@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 751 tests  ±0   1 559 ✅ ±0   6m 9s ⏱️ +58s
   11 suites ±0     192 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit aed123c. ± Comparison against base commit c88a019.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 284 tests  ±0   1 279 ✅ ±0   6m 29s ⏱️ -18s
   11 suites ±0       5 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit aed123c. ± Comparison against base commit c88a019.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 221 tests  ±0   1 221 ✅ ±0   6m 4s ⏱️ -20s
   11 suites ±0       0 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit aed123c. ± Comparison against base commit c88a019.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

   11 files  ±0     11 suites  ±0   6m 23s ⏱️ + 1m 26s
1 977 tests ±0  1 869 ✅ ±0  108 💤 ±0  0 ❌ ±0 
2 127 runs  +1  2 019 ✅ +1  108 💤 ±0  0 ❌ ±0 

Results for commit aed123c. ± Comparison against base commit c88a019.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

697 tests  +7   697 ✅ +7   14m 8s ⏱️ +21s
 10 suites ±0     0 💤 ±0 
 10 files   ±0     0 ❌ ±0 

Results for commit aed123c. ± Comparison against base commit c88a019.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

1 941 tests  ±0   1 939 ✅ ±0   6m 36s ⏱️ +8s
   11 suites ±0       2 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit aed123c. ± Comparison against base commit c88a019.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   65 files  ±0     65 suites  ±0   45m 51s ⏱️ + 2m 14s
8 871 tests +7  8 564 ✅ +7  307 💤 ±0  0 ❌ ±0 
9 021 runs  +8  8 714 ✅ +8  307 💤 ±0  0 ❌ ±0 

Results for commit aed123c. ± Comparison against base commit c88a019.

♻️ This comment has been updated with latest results.

@rbuergi

rbuergi commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

The shard-0 red is pre-existing on main — not this PR

OrleansBrokenNodeTypeAccessTest.Subscribe_ToLayoutAreaOf_NonCompilingNodeTypeInstance_SurfacesError_NotWedge failed on run 31358456796. It sits in the code this PR touches, so I did not wave it off — I checked main directly:

Run Branch When (UTC) Result
31336834864 main 08-09 21:26 ❌ Instance_OfNonCompilingNodeType_AnswersTerminalError_NotSilence
31356206042 main 08-10 04:40 ❌ Subscribe_ToLayoutAreaOf_…
31357286162 main 08-10 05:01 ❌ Instance_OfNonCompilingNodeType_…
31358230786 main 08-10 05:19 ❌ Subscribe_ToLayoutAreaOf_…
31359416118 main 08-10 05:41 ✅
31358456796 this PR 08-10 05:23 ❌ Subscribe_ToLayoutAreaOf_…

Both tests in that class have been flapping on main since at least 2026-08-09 21:26Z — roughly eight hours before this branch existed. It is a live main-red, not a regression here.

Why this change cannot be the cause

The test accepts either an OnError carrying a CompilationFailed NACK or an OnNext that renders the compilation-error overlay, with a 45 s budget. The self-referential cache subscription this PR neutralises fires at the cache hub's 60 s RequestTimeout — past the budget — so the OnError branch was never the path that made this test green. Before and after, the passing path is the overlay, which this PR does not touch. The failure mode is a wedge: nothing surfaced at all within 45 s.

What I did

Merged current main (this branch was behind, which strict_required_status_checks_policy requires anyway), reconciled with a concurrent main-merge someone else had pushed to this branch at 05:23Z (merged, not force-pushed), and re-verified locally:

  • dotnet build -c Release -warnaserror — 0 warnings, 0 errors
  • 20/20 grain tests green, including both OrleansBrokenNodeTypeAccessTest tests and the 7 new virtual-time ones

Fresh CI is running on aed123c55. Not merging — sequencing is yours.

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.

2 participants