Skip to content

fix(#2679): the permission fold resolves its services once; a render on a disposed scope is a teardown race, not two errors - #2728

Merged
rbuergi merged 1 commit into
mainfrom
fix/2679-disposed-scope-resolution
Aug 30, 2026
Merged

rbuergi merged 1 commit into
mainfrom
fix/2679-disposed-scope-resolution

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

What was failing

Two fail lines for one hub disposal (memex-cloud, 2026-08-29):

  1. PermissionEvaluator.GetEffectivePermissions resolved IMeshNodeStreamCache inside its SelectMany selector — the recursive Public leg (:237 → :126) and GetRole. A permission fold is long-lived by design (it re-emits on every AccessAssignment change), so when the hub's Autofac scope was disposed while the fold was still subscribed, the next emission threw ObjectDisposedException straight into the render chain → Rendering failed for area Overview at Error.
  2. LayoutAreaHost.RenderRenderingError then tried to render the error placeholder, whose copy is localised through host.Hub.ServiceProvider.GetService<AccessService>() — the same dead scope → a second ObjectDisposedException, Failed to render the error placeholder. AreaErrorClassifier.IsHubDisposalRace only matched the typed HubDisposingException, so the DI-scope shape of the race was reported as an engineering fault and auto-filed.

What changed

  • PermissionEvaluator — every service the fold needs is resolved once, at the entry point, into a private FoldServices record carried through the recursion (GetEffectivePermissionsCore); the Public leg, GetRole, ObserveAllRoleNodes and ObserveAllMembershipNodes take the already-resolved cache. A subscribed fold never touches hub.ServiceProvider again. Public signatures are unchanged.
  • ScopeTeardown (new, MeshWeaver.Messaging.Hub) — the one probe-gated classifier for "this ObjectDisposedException is the hub's own scope going away": IsScopeTeardown(ex, probe), hub.IsServiceScopeDisposed(), hub.IsTerminatedByScopeTeardown(ex). MessageHub's private Hub init BuildupAction resolves the workspace from a disposed Autofac LifetimeScope, permanently failing the hub #2444 pair and RoutingGrain's RoutingGrain routing and NACK delivery execute after the Autofac root scope is disposed at silo shutdown #2638 copy now delegate to it — one shape, one meaning.
  • Fold residual — if the fold still faults with an ObjectDisposedException while its hub's scope no longer resolves, it terminates as the typed HubDisposingException (new overload carrying the original as inner exception), which the layout host, MessageService and CheckPermissionOutcome already classify as benign teardown. Gated on the probe: an unrelated disposed dependency on a live scope still faults the fold as the defect it is.
  • AreaErrorClassifier.IsHubDisposalRace(ex, scopeDisposed) — the DI-scope shape of the race. LayoutAreaHost probes its own scope once per failure in RenderRenderingError and FailRendering, logs the race at Debug (like the LayoutAreaHost fails an area render instead of retrying a hub that is deactivating #2255 arm), and renders no placeholder on a dead scope — nothing to localise with, nobody to render for; the client's own resubscribe renders the real content.
  • Doc: PermissionApi.md ("Denied vs couldn't decide") describes the disposing-hub leg. What's New entry (Category: Fix).

Tests

  • PermissionFoldAfterScopeTeardownTest (Security) — subscribes the real fold on a hosted hub, disposes that hub's scope out of band (the Hub init BuildupAction resolves the workspace from a disposed Autofac LifetimeScope, permanently failing the hub #2444 mechanism), then lands a Viewer grant. Red before (the re-emission resolves from the dead scope → ObjectDisposedException out of the fold), green after. Also pins the probe's negative control (a disposed dependency on a live scope is not a teardown).
  • ScopeTeardownRenderTest (Layout) — resumes a parked view generator from Autofac's own CurrentScopeEnding (flag set, nothing disposed yet), so it resolves from the dead scope and throws the real Autofac fault into the real render chain. Exactly one LayoutAreaHost record: Debug, carrying the ObjectDisposedException; nothing at Warning or above; no placeholder attempt. Plus the classifier's negative controls.
  • Neighbours re-run green locally: HubDisposalRenderTransientTest, RenderFailureDiagnosticsTest, AreaErrorClassifierTest, SubscribeDuringRecycleTest (77), ScopeDisposedDuringInitializationTest + init siblings (3), RoutingAfterScopeTeardownTest (5), SecurityServiceTests/GroupPermissionTests/PermissionFoldUnavailableTest/NodeTypeGateTests/AdminPartitionAdminTests/UserPublicReadTest/HubCredentialReadAccessTest (57). Every touched project and its dependents built -c Release -warnaserror, 0 Error(s).

Closes #2679

🤖 Generated with Claude Code

https://claude.ai/code/session_01EPCWramjFYquX5SaMdeJKW

…der on a disposed scope is a teardown race, not two errors

PermissionEvaluator.GetEffectivePermissions resolved IMeshNodeStreamCache (and
friends) INSIDE its SelectMany selector — the recursive Public leg and GetRole —
so every emission of a long-lived fold went back to the hub's Autofac scope.
When that scope was disposed while the fold was still subscribed (a hub
deactivating, an area disposing, a rolling deploy), the next emission threw
Autofac's ObjectDisposedException straight into the render chain. The layout
host then reported it at Error ("Rendering failed for area Overview") and
tried to localise its placeholder through the same dead scope, faulting a
second time ("Failed to render the error placeholder").

Root cause fixed, not suppressed:

- PermissionEvaluator: every service the fold needs is resolved ONCE, at the
  entry point, into a FoldServices record carried through the recursion; the
  Public leg, GetRole and the global role/membership queries take the
  already-resolved cache. Public signatures unchanged.
- ScopeTeardown (Messaging.Hub): the ONE probe-gated classifier for "this
  ObjectDisposedException is the hub's own scope going away". MessageHub's
  private #2444 pair and RoutingGrain's #2638 copy now delegate to it.
- The fold's residual fault on a disposed scope terminates as the typed
  HubDisposingException (now with an inner-exception overload), which every
  consumer already classifies as benign teardown. Gated on the probe — an
  unrelated disposed dependency on a live scope still faults the fold.
- AreaErrorClassifier.IsHubDisposalRace(ex, scopeDisposed): the DI-scope shape
  of the race. LayoutAreaHost probes its own scope once per failure, logs the
  race at Debug, and renders NO placeholder on a dead scope (nothing to
  localise with, nobody to render for — the client resubscribes).

Tests: PermissionFoldAfterScopeTeardownTest (Security) disposes a hosted hub's
scope out of band and lands a grant — red before (ObjectDisposedException out
of the fold), green after. ScopeTeardownRenderTest (Layout) resumes a parked
generator from Autofac's CurrentScopeEnding, inside the incident window — one
Debug record, no Error, no placeholder attempt; plus the classifier's
negative controls.

Closes #2679

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EPCWramjFYquX5SaMdeJKW
Copilot AI lite review requested due to automatic review settings August 30, 2026 07:04

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

This PR hardens permission evaluation and layout-area error handling against hub DI-scope teardown races, so a disposal during rendering is treated as routine lifecycle rather than producing duplicate error logs and a failed placeholder render.

Changes:

  • Refactors PermissionEvaluator.GetEffectivePermissions to resolve fold dependencies once at entry and carry them through recursion, avoiding service resolution inside long-lived reactive selectors.
  • Introduces a shared, probe-gated ScopeTeardown classifier for “ObjectDisposedException due to scope teardown” and reuses it from MessageHub, RoutingGrain, permission folds, and layout error classification.
  • Updates LayoutAreaHost/AreaErrorClassifier to recognize the DI-scope teardown shape and avoid rendering a localized placeholder when the host scope is already disposed; adds targeted regression tests and docs/What’s New.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated no comments.

Show a summary per file
File Description
test/MeshWeaver.Security.Test/PermissionFoldAfterScopeTeardownTest.cs New regression test ensuring a subscribed permission fold doesn’t resolve from a disposed hub scope and classifies teardown correctly.
test/MeshWeaver.Layout.Test/ScopeTeardownRenderTest.cs New regression test that deterministically triggers an Autofac scope-ending race and asserts only benign (Debug) reporting with no placeholder attempt.
src/MeshWeaver.Messaging.Hub/ScopeTeardown.cs New shared, probe-gated classifier and IMessageHub extensions for detecting DI-scope teardown.
src/MeshWeaver.Messaging.Hub/MessageHub.cs Replaces private scope-teardown helpers with the shared ScopeTeardown classifier.
src/MeshWeaver.Messaging.Contract/HubDisposingException.cs Adds overload to wrap the original disposed-scope exception as InnerException for downstream teardown classification.
src/MeshWeaver.Mesh.Contract/Security/PermissionEvaluator.cs Moves fold service resolution to a single entry-point resolution and threads resolved services through recursive/public/custom-role legs; probe-gated teardown rethrow as HubDisposingException.
src/MeshWeaver.Layout/Composition/LayoutAreaHost.cs Probes host scope once per failure, widens teardown classification to include the DI-scope shape, and skips placeholder rendering on a dead scope.
src/MeshWeaver.Layout/AreaErrorClassifier.cs Adds probe-gated overload to classify bare ObjectDisposedException as a hub disposal race only when the host scope probe confirms teardown.
src/MeshWeaver.Hosting.Orleans/RoutingGrain.cs Delegates scope-teardown probing/classification to the shared ScopeTeardown implementation.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-30-a-view-that-races-a-restart-no-longer-reports-two-errors.md What’s New entry describing the user-visible improvement.
src/MeshWeaver.Documentation/Data/Architecture/PermissionApi.md Documents the disposing-hub lifecycle behavior and the probe-gated teardown classification.

💡 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 3)

693 tests   501 ✅  4m 39s ⏱️
  5 suites  192 💤
  5 files      0 ❌

Results for commit 15de552.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

941 tests   941 ✅  5m 17s ⏱️
  6 suites    0 💤
  6 files      0 ❌

Results for commit 15de552.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

   10 files     10 suites   5m 37s ⏱️
3 470 tests 3 470 ✅ 0 💤 0 ❌
4 109 runs  4 109 ✅ 0 💤 0 ❌

Results for commit 15de552.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

913 tests   913 ✅  5m 29s ⏱️
  6 suites    0 💤
  6 files      0 ❌

Results for commit 15de552.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 533 tests   1 533 ✅  5m 36s ⏱️
    8 suites      0 💤
    8 files        0 ❌

Results for commit 15de552.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

818 tests   814 ✅  7m 36s ⏱️
  6 suites    4 💤
  6 files      0 ❌

Results for commit 15de552.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

   41 files     41 suites   34m 16s ⏱️
8 368 tests 8 172 ✅ 196 💤 0 ❌
9 007 runs  8 811 ✅ 196 💤 0 ❌

Results for commit 15de552.

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.

PermissionEvaluator and the layout error placeholder resolve services from the hub's DI scope after it has been disposed

2 participants