Repository navigation
RoutingGrain routing and NACK delivery execute after the Autofac root scope is disposed at silo shutdown #2638
Description
What is failing
RoutingGrain.PostFailure — the router's NACK leg — cannot deliver a DeliveryFailure to a pod-process-hub sender during silo shutdown. Both transports fail: the directed pod-hub grain call throws because grain-proxy construction resolves a service through the pod's Autofac root LifetimeScope, which is already disposed, and the stream fallback then hits the identical disposed-scope exception on its own service resolution. The delivery being NACKed had failed one layer earlier for the same reason (the original delivery to its target hub failed resolving services on the disposed scope), so the entire exchange — delivery and NACK — is one symptom: routing work executing after the process's DI container is dead.
Probable cause
High confidence on the mechanism. The aggregate carries the same ObjectDisposedException twice (Autofac LifetimeScope.ThrowDisposedException()), driven through GrainFactory.GetGrain<T>(…) → generated proxy → AutleansGeneratedCodeHelper.GetService → AutofacServiceProvider.GetService from PostFailure's directed-NACK lambda, and the same exception again on the stream-publish arm. That is only possible if the root Autofac scope was disposed while the RoutingGrain (a [StatelessWorker]) still had a routing turn in flight — i.e. the silo shutdown sequence disposes the service provider before routing work is quiesced, and the routing legs have no teardown gate. The hub layer solved exactly this class for MessageService.Post with a RunLevel >= DisposeHostedHubs short-circuit and a disposed-scope swallow in CaptureCallerAccessContext; the Orleans routing/NACK path has neither. Medium confidence on which exact lifecycle stage disposes the scope while a routing turn is still executing — that needs confirming in the host stop sequence.
Impact
A single burst on one pod at one instant during shutdown — a race in the teardown window, not a storm, and no recurrence since. Each hit strands a pod-process-hub sender for its full reply budget (~60 s). Practical impact is limited when the strandees live on the dying pod itself, but a cross-pod sender waiting on a reply from the shutting-down pod is stranded for that budget, and every rolling deploy re-creates the window. Worth fixing so the shutdown path fails clean (typed ShuttingDown, no dual-transport AggregateException) and the wedge budget stays bounded.
Where to look
MeshWeaver.Hosting.Orleans.RoutingGrain.PostFailure— the<>c__DisplayClass30_0.<PostFailure>b__2()directed-NACK lambda and its stream fallback; both resolve services through the process root scope.- The silo host stop sequence that disposes the Autofac root
LifetimeScopewhileRoutingGrainturns (and the target hub's handlers) are still executing. - The precedent fix for the same defect class:
MessageService.Post'sRunLevelgate andSynchronizationStream.CaptureCallerAccessContext's disposed-scope swallow — see Hub Disposal Model ("Teardown-safe writes") and Error Propagation & Wedges. - Transport context: Pod-Hub Delivery — the Transport Swap and Orleans Stream Pub-Sub Durability.
Not a duplicate of the directed-delivery grain-directory failures tracked as #2357 (the "Directed delivery to pod hub … failed" incidents — Known status: Dead / hop-limit exceptions): those are the forward leg hitting an unstable grain directory during membership changes, while this is the NACK leg failing on a disposed DI scope during process teardown. Also distinct from #2172 (NACK-of-NACK to a subscriber-less sender): there both transports would succeed-into-the-void; here both fail hard on the disposed scope.
Evidence
| Fingerprint | 84a2622ace10991d |
| Category | MeshWeaver.Hosting.Orleans.RoutingGrain |
| Severity | Error |
| Exception | System.AggregateException |
| Top frame | Autofac.Core.Lifetime.LifetimeScope.ThrowDisposedException() |
| Namespace | memex |
| Pods | memex-portal-deployment-7ccdd994cf-sbrjg |
| Occurrences | 2 |
| First seen | 2026-08-29 06:09:38Z |
| Last seen | 2026-08-29 06:09:38Z |
Recent log lines
2026-08-29 06:09:38Z memex-portal-deployment-7ccdd994cf-sbrjg fail: MeshWeaver.Hosting.Orleans.RoutingGrain[0]
[ROUTE] Cannot deliver the Failed DeliveryFailure for EEdtn1YF2k6woBez81rZOA to sender cache/j3iTgVzDfk-XzzqJP5wM7g: BOTH transports failed — the directed call to its grain/pod-hub activation AND the stream publish. The sender will wait out its own request budget — the original failure was: Delivery to 'Planning' failed: Instances cannot be resolved and nested lifetimes cannot be created from this LifetimeScope as it (or one of its parent scopes) has already been disposed.. 0 earlier undeliverable NACK(s) to this sender since the last such line were logged at Debug.
System.AggregateException: Neither the directed pod-hub call nor the stream publish could carry the NACK. (Instances cannot be resolved and nested lifetimes cannot be created from this LifetimeScope as it (or one of its parent scopes) has already been disposed.) (Instances cannot be resolved and nested lifetimes cannot be created from this LifetimeScope as it (or one of its parent scopes) has already been disposed.)
---> System.ObjectDisposedException: Instances cannot be resolved and nested lifetimes cannot be created from this LifetimeScope as it (or one of its parent scopes) has already been disposed.
at Autofac.Core.Lifetime.LifetimeScope.ThrowDisposedException()
at Autofac.Extensions.DependencyInjection.AutofacServiceProvider.GetService(Type serviceType)
at Orleans.Serialization.GeneratedCodeHelpers.OrleansGeneratedCodeHelper.GetService[TService](Object caller, ICodecProvider codecProvider) in /_/src/Orleans.Serialization/GeneratedCodeHelpers/OrleansGeneratedCodeHelper.cs:line 58
at Proxy_IPodHubGrain(Object, GrainReferenceShared, IdSpan)
at Orleans.GrainFactory.GetGrain[TGrainInterface](String primaryKey, String grainClassNamePrefix) in /_/src/Orleans.Core/Core/GrainFactory.cs:line 61
at MeshWeaver.Hosting.Orleans.RoutingGrain.<>c__DisplayClass30_0.<PostFailure>b__2() in /home/r…[truncated]
2026-08-29 06:09:38Z memex-portal-deployment-7ccdd994cf-sbrjg fail: MeshWeaver.Hosting.Orleans.RoutingGrain[0]
[ROUTE] Cannot deliver the Failed DeliveryFailure for mZORPARyWUWGqr140-QpFQ to sender portal/nodeops-j3iTgVzDfk-XzzqJP5wM7g: BOTH transports failed — the directed call to its grain/pod-hub activation AND the stream publish. The sender will wait out its own request budget — the original failure was: Delivery to 'Planning' failed: Instances cannot be resolved and nested lifetimes cannot be created from this LifetimeScope as it (or one of its parent scopes) has already been disposed.. 0 earlier undeliverable NACK(s) to this sender since the last such line were logged at Debug.
System.AggregateException: Neither the directed pod-hub call nor the stream publish could carry the NACK. (Instances cannot be resolved and nested lifetimes cannot be created from this LifetimeScope as it (or one of its parent scopes) has already been disposed.) (Instances cannot be resolved and nested lifetimes cannot be created from this LifetimeScope as it (or one of its parent scopes) has already been disposed.)
---> System.ObjectDisposedException: Instances cannot be resolved and nested lifetimes cannot be created from this LifetimeScope as it (or one of its parent scopes) has already been disposed.
at Autofac.Core.Lifetime.LifetimeScope.ThrowDisposedException()
at Autofac.Extensions.DependencyInjection.AutofacServiceProvider.GetService(Type serviceType)
at Orleans.Serialization.GeneratedCodeHelpers.OrleansGeneratedCodeHelper.GetService[TService](Object caller, ICodecProvider codecProvider) in /_/src/Orleans.Serialization/GeneratedCodeHelpers/OrleansGeneratedCodeHelper.cs:line 58
at Proxy_IPodHubGrain(Object, GrainReferenceShared, IdSpan)
at Orleans.GrainFactory.GetGrain[TGrainInterface](String primaryKey, String grainClassNamePrefix) in /_/src/Orleans.Core/Core/GrainFactory.cs:line 61
at MeshWeaver.Hosting.Orleans.RoutingGrain.<>c__DisplayClass30_0.<PostFailure>b__2() i…[truncated]
Opened automatically from Admin/_LogIncident/84a2622ace10991d. Recurrences are folded into this issue rather than opening new ones.
Activity
rbuergi commentedon Aug 29, 2026
PR #2647 — the window now fails clean and correctly classified. It does NOT close the window.
Your mechanism is confirmed from source, and the diagnosis was right on both counts. Two things landed; one is deliberately left, and it is the one this issue rates at medium confidence.
Fixed
1. The classification, which is the functional half.
Delivery to 'Planning' failed: Instances cannot be resolved …was NACK'dErrorType.Failed— terminal — becauseClassifyDeliveryExceptionrecognised onlyIsDirectoryUnstableandIsShutdownShaped. Consumers with recovery machinery of their own (SynchronizationStream's resubscribe latch,MeshNodeStreamCache's transient-owner rule) ride outShuttingDownand tear down on a terminal verdict, so a routine pod exit permanently killed mirrors that would have resumed on the surviving pod. That is precisely the damage #2346/#2357 removed for the other two shapes and left standing for this one.It is gated on a probe of the container, not on the bare
ObjectDisposedExceptiontype test — the same shapeMessageHub.IsTerminatedByScopeTeardownuses for #2444. An unrelated disposed dependency is a genuine defect and stays terminal; a negative control pins that.2. The dual-transport
AggregateException. Exactly as you called it: the stream fallback resolves through the same dead scope, so it could only restate the first fault. It is now skipped when the first transport died on a disposed container, and the outcome is stated once atInformationnaming the cause. A process finishing its shutdown is not an incident.Left open — and it is the root cause, so please read this before closing
I did not resolve that, and I did not want to guess at it, because the fix it implies is a change to the host stop sequence rather than to the router: routing work would have to be quiesced before the container is disposed. The precedent you cite (
MessageService.Post'sRunLevelgate) is a hub-level short-circuit; the equivalent here is not obvious, and two candidate shapes are meaningfully different:RoutingGrain— but the gateOrleansRoutingService.GrainWhileRunningimplements is documented as deliberately not applied to the drain: a message already accepted for routing must still land, and Orleans' own placement is what sends it to a healthy silo. Refusing there would drop live work rather than relocate it;The precise open question: at which stage of the silo host stop sequence is the Autofac root
LifetimeScopedisposed relative toRoutingGrainturn quiescence, and is there a lifecycle hook that can hold disposal until the routing pool has drained without deadlocking on it (theIMeshService-constructed-with-the-calling-hub hazard)? Until that is answered, #2647 makes the window bounded, quiet and correctly classified — but the window is still there, and every rolling deploy still re-creates it.Not #2635
Worth recording since the two look adjacent: #2635 / PR #2644 is the test-harness
DisposalCompletedjoin. Different code path, no file overlap, and it does not reach this.Merge pull request #2647 from Systemorph/fix/2638-routing-after-scope…
rbuergi commentedon Aug 30, 2026
The open question, answered from source — and the seam it names (PR #2726)
The root goes LAST, and the ordering the comments claim is real. The host's root container is the mesh's Autofac container (
MeshHostApplicationBuilder.cs:46→MessageHubServiceProviderFactory→ServiceProviderExtensions.cs:57-60). The root hub never disposes it (MessageHub.cs:2295, deliberately commented out). The host disposes it inRunAsync'susing, afterStopAsynchas firedApplicationStopping, stopped every hosted service in reverse order —SiloHostedServicebeforeMeshTeardownHostedService, Kestrel after both (OrleansServerRegistryExtensions.cs:39-45) — and runStoppedAsync, whereMeshTeardownHostedServicedrains the mesh root hub. Inside the silo stop (Orleans 10.2.2Silo.Participate,Silo.cs:466-486;LifecycleSubject.OnStopdescending, same stage in parallel):Active20000 →BecomeActive19999 (membershipShuttingDown) →GrainDeactivation19998 (Catalog.DeactivateAllActivations, 30 sDeactivationTimeoutper activation) →RuntimeServices4000 (BlockApplicationMessages) →RuntimeInitialize2000 (message centre stop) →First(IoPoolSiloTeardownjoin).So the window is not "the scope goes too early" — it is "nothing ever waits for a route". Three tiers, none joined:
RoutingGrain.RouteMessageis O(1) and returnsForwarded(One non-reentrant RouteMessage can wedge a whole silo's routing — no timeout on that path #1028); Orleans never sees a request in flight on the routing grain, soDeactivateAllActivationsdeactivates it instantly and theDeactivationTimeoutwait never applies. The "a turn parked inStreamPostTimeout/ retries / the subscriber probe outlives the deactivation budget" hypothesis is false — there is no such turn.SubscribeThroughPoolholds its permit for the subscribe window only (IoPool.cs:391-440); past that,IoPoolSiloTeardown's join sees nothing, and the drain at stageFirstdisposes the leg and completes it silently.DeliverToGrainWithRetry) was subscribed insideBuildGrainRoute's.Selectwith the subscription discarded, and everyPostFailureNACK was a bare.Subscribe— detached from even the leg. Their tails (grain call, ≤6 retry timers onScheduler.Default, the NACK) ran through grain deactivation, the message-centre stop, the mesh drain and the container disposal. That is exactly the prod trace:Delivery to 'Planning' failed: Instances cannot be resolved…is a retry'sGetGrainresolving the codec provider from the disposed scope, then the NACK about it dying the same way.The hook. A silo lifecycle participant at
ServiceLifecycleStage.Active— the first thing the stop does, before membership announcesShuttingDownand before a single grain deactivates — holding on a mesh-scoped gauge (RoutingQuiescence) that counts every leg and NACK from dispatch to termination. It cannot deadlock: the wait is a subscription bridged once at the boundary (ReactiveCompletion.ObserveCompletion, not.ToTask()), it parks nothing a leg needs (legs run on pool/thread-pool threads, a fully running scheduler answers their grain calls, their NACKs land on hubs that are disposed only inStoppedAsync), and it converges becauseApplicationStoppingfires before any hosted service stops, soOrleansRoutingService.DeliverMessagealready refuses new dispatches — the count is monotone non-increasing from the first stop step. Bounded at 30 s, residual named at Error; a non-graceful stop holds nothing.Not a gate — nothing is refused. This is the "ordering" shape you asked for: accepted work lands, over a transport that still exists, and the per-node delivery is now composed into the leg so the pool drain terminates whatever the hold released over. Nothing routing-related executes after the silo has stopped, let alone after the container is gone.
Pinned by
RoutingQuiescenceTest(realSiloLifecycleSubject, a probe at the grain-deactivation stage; mutation-verified — the pre-fix shape fails at "the silo stop must be HELD … but found True") andRoutingQuiescenceClusterTest(a stalled leg is counted on the silo's gauge until it terminates).fix(#2638): hold the silo stop until accepted routing work has landed (…
fix(#2679): the permission fold resolves its services once, and a ren…
systemorph-com commentedon Sep 17, 2026
Reopened — this fault is happening again.
Still occurring: 5 more since the last update (7 total).
memex-portal-deployment-7ccdd994cf-sbrjg,memex-portal-deployment-7c6b48957d-xml97Recent log lines
rbuergi commentedon Sep 17, 2026
Re-closed — the reopen above was on stale evidence (#4577).
The recurrence bot reopened this at
2026-09-17T07:52:57Zunder the headline "this fault is happening again", but the occurrence its own comment cites is OLDER than the close it reverted:Last seen:2026-08-29 06:37:19Z2026-08-30T07:14:35ZSo the counter moved, not the clock — the fault's newest occurrence predates the close, which is the one thing "still happening" has to mean. Root cause and fix:
LogIncidentFiler.Reopenreopened on the sole condition that the issue was closed, while the comment-debt clock pays a final count for incidents whose fault has stopped (the fix, Systemorph/MeshWeaver.Plugins#2014, is written but NOT yet merged — it makes a reopen requireincident.LastSeen > issue.ClosedAt).This restores the close exactly as it stood; nothing is asserted about the defect itself. If the fault happens again, the next occurrence postdates this close and reopens the issue on evidence that means what it says.
One of 53 issues whose reopen was measured stale (129 bot reopens on 2026-09-17, 125 of them inside the 07:51-07:53Z sweep; 76 postdate their close and were left open). Measured and re-closed 2026-09-17 by Claude Opus 5.