Repository navigation
fix(#2679): the permission fold resolves its services once; a render on a disposed scope is a teardown race, not two errors - #2728
Conversation
…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
There was a problem hiding this comment.
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.GetEffectivePermissionsto resolve fold dependencies once at entry and carry them through recursion, avoiding service resolution inside long-lived reactive selectors. - Introduces a shared, probe-gated
ScopeTeardownclassifier for “ObjectDisposedException due to scope teardown” and reuses it fromMessageHub,RoutingGrain, permission folds, and layout error classification. - Updates
LayoutAreaHost/AreaErrorClassifierto 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.
Test Results (shard 3)693 tests 501 ✅ 4m 39s ⏱️ Results for commit 15de552. |
Test Results (shard 2)941 tests 941 ✅ 5m 17s ⏱️ Results for commit 15de552. |
Test Results (shard 4) 10 files 10 suites 5m 37s ⏱️ Results for commit 15de552. |
Test Results (shard 0)913 tests 913 ✅ 5m 29s ⏱️ Results for commit 15de552. |
Test Results (shard 5)1 533 tests 1 533 ✅ 5m 36s ⏱️ Results for commit 15de552. |
Test Results (shard 1)818 tests 814 ✅ 7m 36s ⏱️ Results for commit 15de552. |
Test Results 41 files 41 suites 34m 16s ⏱️ Results for commit 15de552. |
What was failing
Two
faillines for one hub disposal (memex-cloud, 2026-08-29):PermissionEvaluator.GetEffectivePermissionsresolvedIMeshNodeStreamCacheinside itsSelectManyselector — the recursive Public leg (:237→:126) andGetRole. A permission fold is long-lived by design (it re-emits on everyAccessAssignmentchange), so when the hub's Autofac scope was disposed while the fold was still subscribed, the next emission threwObjectDisposedExceptionstraight into the render chain →Rendering failed for area Overviewat Error.LayoutAreaHost.RenderRenderingErrorthen tried to render the error placeholder, whose copy is localised throughhost.Hub.ServiceProvider.GetService<AccessService>()— the same dead scope → a secondObjectDisposedException,Failed to render the error placeholder.AreaErrorClassifier.IsHubDisposalRaceonly matched the typedHubDisposingException, 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 privateFoldServicesrecord carried through the recursion (GetEffectivePermissionsCore); the Public leg,GetRole,ObserveAllRoleNodesandObserveAllMembershipNodestake the already-resolved cache. A subscribed fold never toucheshub.ServiceProvideragain. Public signatures are unchanged.ScopeTeardown(new,MeshWeaver.Messaging.Hub) — the one probe-gated classifier for "thisObjectDisposedExceptionis 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 andRoutingGrain'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.ObjectDisposedExceptionwhile its hub's scope no longer resolves, it terminates as the typedHubDisposingException(new overload carrying the original as inner exception), which the layout host,MessageServiceandCheckPermissionOutcomealready 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.LayoutAreaHostprobes its own scope once per failure inRenderRenderingErrorandFailRendering, 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.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 →ObjectDisposedExceptionout 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 ownCurrentScopeEnding(flag set, nothing disposed yet), so it resolves from the dead scope and throws the real Autofac fault into the real render chain. Exactly oneLayoutAreaHostrecord: Debug, carrying theObjectDisposedException; nothing at Warning or above; no placeholder attempt. Plus the classifier's negative controls.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