Repository navigation
refactor(runtime): make activation states symmetric - #11652
ReubenBond wants to merge 9 commits into
Conversation
Replace ActivationState with a linear chain which is symmetric around Active. Each setup phase pairs with the teardown phase which undoes it, teardown starts at the mirror of the last completed setup phase, a deactivation request during setup is recorded intent which diverts setup at its next phase boundary, and forced disposal is an explicit transition to Disposing. SetState asserts each transition. Part of dotnet#8812.
There was a problem hiding this comment.
🟡 Changes recommended
Forced disposal can trigger rehydration callbacks on disposed participants, and the regression test leaves an activation registered with deactivation incomplete, causing fixture shutdown to wait until timeout.
2 open issues
What changed in this PR
Refactors Orleans activation handling into a state chain symmetric around Active, pairing setup phases with teardown phases. This prepares for #11650’s activation-cancellation fix without introducing that behavior change here.
Changes:
- Separates deactivation requests from lifecycle state and consolidates teardown into a phase-based pipeline.
- Moves rehydration after registration and updates runtime state checks.
- Adds model-based and regression tests, and documents the lifecycle changes.
| File | Description |
|---|---|
test/Orleans.Runtime.Internal.Tests/Orleans.Runtime.Internal.Tests.csproj |
Adds Accordant for model-based tests. |
test/Orleans.Runtime.Internal.Tests/ActivationsLifeCycleTests/RetirementHandoffTests.cs |
Updates state and deactivation-request assertions. |
test/Orleans.Runtime.Internal.Tests/ActivationsLifeCycleTests/ActivationStateMachineTests.cs |
Adds transition, lifecycle-model, disposal, and completion-race tests. |
test/Orleans.Runtime.Internal.Tests/ActivationsLifeCycleTests/ActivationDataMigrationTests.cs |
Covers registration boundaries, rehydration ordering, and early deactivation. |
test/Orleans.Runtime.Internal.Tests/ActivationsLifeCycleTests/ActivationDataLifecycleTests.cs |
Updates the terminal-state assertion. |
src/Orleans.Runtime/GrainDirectory/DistributedGrainDirectory.cs |
Uses termination intent when filtering activations. |
src/Orleans.Runtime/Core/GrainRuntime.cs |
Rejects runtime access during disposal and after destruction. |
src/Orleans.Runtime/Catalog/ActivationState.cs |
Defines symmetric states and transition rules. |
src/Orleans.Runtime/Catalog/ActivationData.cs |
Implements phased setup, teardown, and rehydration ownership. |
docs/site/src/content/docs/implementation/activation-lifecycle.md |
Documents the state chain and revised migration ordering. |
Review effort: Balanced
Setup boundaries stop setup without entering teardown; the deactivation operation alone maps the last completed phase to its teardown entry. ActivateAsync and FinishDeactivating become sequences of phase methods, and teardown is one loop over its phases. Rehydration-context ownership moves to a single TakeRehydrationContext helper.
…ed disposal Recheck Rehydrating under the lock before invoking migration participants, so forced disposal which preempts rehydration skips them. Allow an activation operation queued before a pre-start deactivation request to find the activation in None. Remove the forcibly disposed activation from the catalog in the disposal test, as ActivationDirectory disposal does.
There was a problem hiding this comment.
🟡 Changes recommended
The unscoped rehydration listener can dispose the target activation during registration in response to a concurrent migration, allowing the test to pass without exercising the intended lifecycle gap.
1 open issue
4 resolved since last review
Review effort: Balanced
…ctivation The process-wide activity listener now disposes only when the rehydrate span starts on the target activation's scheduler, and the test asserts the activation was Rehydrating at that point.
There was a problem hiding this comment.
🔵 Needs a closer look
Human assessment is needed because critical lifecycle reentrancy and disposal-exception paths can trigger invalid assertions and leave activation resources undisposed.
No open issues 🎉
1 resolved since last review
Review effort: Balanced
…ntrant observers Dispose an unconsumed rehydration context alongside the service scope, so a context whose disposal throws cannot prevent the grain instance and services from being disposed. Deactivate no longer asserts that the activation is not disposing, since lifecycle observers and diagnostic listeners run synchronously inside it and may force disposal.
|
Follow-up on the latest Copilot review summary ("lifecycle reentrancy and disposal-exception paths"), which had no inline threads. Both paths were real; fixed in dcce06e:
|
…ion setup Deactivate and FailSetup record the request, the transition into teardown, and the scheduled deactivation before observers and cancellation callbacks run. RunOnActivateAsync reports observed cancellation, so forced disposal during OnActivateAsync is a cancellation, not a failure. The success tail runs after setup, the activation span ends on every path, a throwing rehydration context no longer fails activation, and teardown after a registration failure no longer receives the canceled activation token. Tests now fail on ActivationData assertion failures which the operation loop swallows, record lifecycle start, stop, and deactivation so teardown entries are exact, and observe directory calls for pre-start deactivation.
…r in teardown Dehydrating discards a migration requested during setup, so an activation whose setup stopped never sends partially initialized state, such as grain state whose storage read was canceled. Unregistering removes the activation's registration regardless of the deactivation reason: a registration which threw may have committed, and unregistration is conditional on the activation's identity. The Accordant model now gates directory registration and unregistration and covers migration requests and forced disposal.
There was a problem hiding this comment.
🔵 Needs a closer look
Pre-start deactivation leaves the activation span unstopped, while reentrant disposal can allow subsequent migration callbacks to run on an activation whose services have already been released.
2 open issues
Review effort: Balanced
ActivationState is byte-backed with None = 0, Active = 127, and Destroyed = 254, so the mirror remains Destroyed - s and new phase pairs can be inserted at mirrored values without renumbering. Transitions use an explicit GetNext rather than arithmetic. Rehydration rechecks Rehydrating before each migration participant, so a participant which forces disposal stops the rest. An activation operation which finds setup stopped before registration ends the activation span. Fixes the missing final newline flagged by the whitespace check.
There was a problem hiding this comment.
🔵 Needs a closer look
The lifecycle refactor requires maintainer validation of reentrancy and concurrent shutdown invariants across net8.0/net10.0, as runtime execution and affected-code coverage evidence were unavailable.
No open issues 🎉
2 resolved since last review
Review effort: Balanced
…e cases Accordant's transition-coverage generation at depth 10 produces about 9,400 overlapping sequences covering 51 model transitions, which took minutes to execute. The test now greedily selects a deterministic subset which covers every transition the generated cases cover (19 cases), running in about a second.
There was a problem hiding this comment.
🔵 Needs a closer look
The coupled lifecycle changes require maintainer review and runtime validation on net8.0 and net10.0 because static review cannot establish cross-silo recovery, shutdown, or forced-disposal race guarantees.
No open issues 🎉
Review effort: Balanced
Code coverage
Report-only conclusion: improved. The current-main baseline is commit Coverage combines every CI test matrix job, including providers, CodeGen, .NET 8/10, Linux, Windows, and macOS, using canonical physical source and branch identities. The comparison remains report-only while normal line and branch variance is calibrated. Coverage details |



Activation state transitions were asymmetric. A deactivation request during setup set the activation's state to
Deactivatingwhile setup code was still running. Teardown then inferred which steps to undo from the state at request time and from special cases such as theDirectoryFailureunregister skip. This refactor makes the activation state machine a linear chain, symmetric aroundActive, so each teardown step pairs with the setup step it undoes. It lands before #11650 (part of #8812) so that fix reduces to the activation-outcome rule.State machine
NoneDestroyedConstructing(instance, previous registration)Disposing(instance, services, leave catalog, forward)RegisteringUnregisteringRehydratingDehydrating(migration)Starting(lifecycle start)Stopping(lifecycle stop)Activating(OnActivateAsync)Deactivating(OnDeactivateAsync)ActiveActivationStateis a byte with spaced values:None = 0,Active = 127,Destroyed = 254, and each teardown phase atDestroyed - sof its setup phase. Group checks are single comparisons againstActive, and a new phase pair can be inserted at mirrored values without renumbering.GetNext).SetStateasserts the transition (ActivationStateExtensions.IsValidTransition).Startingphase enters atStopping.Disposingtolerates partial construction.RegisteringprecedesRehydratingso that only the activation which holds the directory registration installs migrated state.DehydratingprecedesUnregisteringbecause a migrating activation unregisters only if its handoff fails.IsDeactivationRequested), not a state. It cancels the setup token, and setup stops at its next phase boundary, leaving the activation in its last completed phase. The deactivation operation, which runs after setup, is the only place which maps that phase to its teardown entry. The request, any transition into teardown, and the scheduled deactivation operation are recorded before observers and cancellation callbacks run, so a reentrant request finds the first one and a recorded request always completes.DisposeAsync, used when the activation directory is disposed) is an explicit forced transition toDisposing. It skips the phases which have not run.Setup is a sequence of phase methods (
RegisterAsync,RunRehydration,StartLifecycleAsync,RunOnActivateAsync) separated byTryAdvanceSetupboundaries. Teardown is one loop over states, running one method per phase (RunOnDeactivateAsync,StopLifecycleAsync,DehydrateAsync,UnregisterAsync):Deactivating→Stopping→Dehydrating→Unregistering→Disposing→Destroyed.ActivateAsyncandFinishDeactivatingare each about half their size onmain. This replaces thePreviousStatecaptured when deactivation was requested, soOnDeactivateAsyncand retirement migration run exactly when activation completed.Unregisteringalways removes the registration unless it was handed to a migration target; theDirectoryFailurereason no longer decides it.Behavior changes
The aim is to preserve
main's behavior, so the following list covers every change. AnOnActivateAsyncwhich returns normally after its token was canceled still fails activation. That rule changes in #11650.Rehydration follows registration. Migration participants'
OnRehydrateruns after directory registration, duringRehydrating. It used to run before registration. The previous registration is still read while the activation is constructed, so the registration CAS and the cache updates on queued requests are unchanged. When registration finds a competing activation, the target no longer rehydrates state it would discard.Deactivation during registration.
Activatingafter the request.Unregisteringand unregisters it. Onmain, theDirectoryFailurereason skipped unregistration unless a deactivation request preceded the failure, which could leave a committed registration pointing at a destroyed activation. Unregistration is conditional on the activation identity, so it leaves other registrations in place. The same applies to activations whichLegacyGrainDirectorydeactivates when their directory partition owner fails: they now issue an unregistration which finds no matching entry.Migration. Only an activation which completed activation migrates (invariant 4 of the design note). A migration requested during setup stops setup, and the activation deactivates without migrating. On
main, it migrated after setup stopped, which could send state the activation never loaded, such as grain state whose storage read was canceled, to the target as initialized state.TryStartMigrationaccepts requests from lifecycle start until teardown reachesDehydrating, where the migration decision is made. It used to accept requests from registration until disposal, so it could returntruefor an activation which would never migrate.Completion racing a request. A deactivation request which arrives after
OnActivateAsyncreturns and passes its cancellation check, but before the activation becomes active, now runsOnDeactivateAsync. Activation completed, so teardown starts atDeactivating. That activation is reported as canceled: it emits noActivatedevent and no success metric.Forced disposal mid-teardown. Forced disposal during teardown skips the remaining lifecycle stop, dehydration, and unregistration. The in-flight deactivation still leaves the catalog, forwards queued requests, and completes
Deactivated. Forced disposal happens when the silo's services are disposed. Forced disposal during rehydration also skips migration participants which have not run.Deactivation before start. A deactivation requested before the activation starts (for example, from
IConfigureGrainContext) skips construction. Previously, constructing the grain instance threw.Activation span. The activation span now ends when activation ends on every path, including registration failure, where it previously never ended.
State names. State names change in the
orleans.grain.stateactivity tag, in logs, and in activationToString/detailed reports, whichIManagementGrainreports include. The mapping:Creating→None/Constructing/Registering/RehydratingActivating→Starting/ActivatingValid→ActiveDeactivating→DeactivatingthroughUnregisteringInvalid→Disposing/DestroyedA deactivation requested during setup no longer shows as
Deactivatinguntil setup stops. The rehydrate span now follows the directory registration span.ActivationStateis internal, so there is no public API change.Tests
ActivationStateMachineTests:OnActivateAsync,OnDeactivateAsync, lifecycle stop, and directory unregistration wait at gates. It compares each phase, state, and migration-request outcome with the model.Deactivatingafter a failedOnActivateAsyncis caught.ActivityListenerhook.ActivationDataMigrationTests: rehydration only after registration, discarding a migration requested during setup, deactivation during registration (registration commits or throws), deactivation during rehydration, deactivation before start, and the directory events for failed registrations.ActivationAssertionRecorderfails the lifecycle tests onActivationDataassertion failures, which the activation's operation loop otherwise swallows.Follow-up, unchanged from
main: the 5-second back-off scheduled after a failed activation is canceled immediately by the deactivation it precedes.