fix: wrap outer SynchronizationContext in ActorCellKeepingSynchronizationContext#8182
Merged
Aaronontheweb merged 22 commits intoMay 7, 2026
Merged
Conversation
Under xUnit v3 parallel-class scheduling, MaxConcurrencySyncContext dispatches test ctors and bodies onto a dedicated pool of reused threads. TestKitBase's constructor pins InternalCurrentActorCellKeeper.Current (a ThreadStatic) on its ctor thread; when a sibling test's body lands on that same reused thread, the pre-await synchronous prefix of the [Fact] reads the sibling's cell as its implicit sender, causing Tell() to use the wrong Sender. Replies then cross ActorSystem boundaries. Fix: add a BeforeAfterTestAttribute that runs synchronously on the test- body thread and (a) pins Current to the running test's TestActor cell, (b) installs ActorCellKeepingSynchronizationContext so await continuations also re-pin the cell. After the test, Current is cleared so the reused worker doesn't carry the cell into the next test. Applied to Akka.TestKit.Xunit.TestKit so derived test classes (including Akka.Hosting.TestKit downstream) get parallel-safe behavior automatically via attribute inheritance. ActorCellKeepingSynchronizationContext promoted to [InternalApi] public so the attribute can install an equivalent wrapper without duplicating the save/pin/restore logic. TBD XML docs replaced with real documentation. Regression tests: - ParallelAmbientContextSpec: 16 + 8 sibling test classes asserting implicit-sender correctness and INoImplicitSender == null across awaits. Marked [LocalFact] — requires xUnit.ParallelizeTestCollections=true to exercise the bug (disabled in the shared src/xunit.runner.json). - AkkaCleanAmbientContextAttributeSpec: reflection guards that the attribute is declared on the base TestKit, is inherited by subclasses, and has Inherited=true in its AttributeUsage.
…Arkatufus/akka.net into xunit-3-implicit-sender-leak-D2
Keep ActorCellKeepingSynchronizationContext internal via friend assembly access and run Akka.TestKit.Xunit.Tests with parallel collections so CI exercises the implicit-sender leak regression by default.
Stop swallowing NullReferenceException when reading TestActor and drop the reflection-only attribute spec in favor of the parallel behavior tests that now run in CI.
ActorCellKeepingSynchronizationContext now accepts an optional inner SynchronizationContext and delegates Post/Send scheduling to it while wrapping callbacks with the cell-pinning save/restore window. When no inner SC exists (the default), behavior is identical to before. AkkaCleanAmbientContextAttribute captures the active SC in Before() and passes it as the inner SC, then restores it in After(). This preserves xUnit v3's MaxConcurrencySyncContext scheduling, which downstream consumers like Akka.Hosting.TestKit depend on for async IHost lifecycle. Without this, applying the attribute to Hosting's TestKit causes test hangs. See akkadotnet/Akka.Hosting#735 and akkadotnet/Akka.Hosting#733.
Arkatufus
suggested changes
Apr 24, 2026
xUnit v3's runner awaits the test body between Before() and After(), so After() can resume on a different OS thread. ThreadStatic fields set in Before() are invisible on the new thread, causing After() to silently skip cleanup. AsyncLocal flows via ExecutionContext across await boundaries, ensuring correct save/restore regardless of thread.
Arkatufus
previously requested changes
Apr 28, 2026
Aligns with project style guidance (sealed classes and records as default for immutable data carriers). Behavior unchanged — instance is only stored/retrieved through AsyncLocal, never compared. Per review feedback on PR akkadotnet#8182.
This was referenced Jun 12, 2026
Open
Open
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Builds on #8174. Makes
ActorCellKeepingSynchronizationContexta proper decorator that preserves the outerSynchronizationContextinstead of replacing it with rawThreadPooldispatch.Problem: #8174's
Post()dispatches viaThreadPool.QueueUserWorkItem, discarding whatever SC was active when the attribute installed it. This works forAkka.TestKit.Xunittests but hangs downstream consumers likeAkka.Hosting.TestKitwhose asyncIHostlifecycle depends on xUnit v3'sMaxConcurrencySyncContextscheduling. See akkadotnet/Akka.Hosting#733 and akkadotnet/Akka.Hosting#735 for the full investigation.Fix (2 files, backward-compatible):
ActorCellKeepingSynchronizationContext— adds optionalinnerparameter (defaults tonull). When non-null,Post()/Send()delegate scheduling to the inner SC while wrapping callbacks with the cell-pinning window. When null, behavior is identical to [xUnit 3] Fix parallel-class implicit-sender leak in Akka.TestKit.Xunit #8174. AddedCreateCopy()override to preserve the wrapping chain.AkkaCleanAmbientContextAttribute— capturesSynchronizationContext.CurrentinBefore()and passes it as the inner SC. Restores the original SC inAfter(). Added_appliedguard soAfter()is a no-op whenBefore()returned early (non-TestKitBase test class).Depends on
Test plan
ParallelAmbientContextSpec(16 implicit-sender + 8 no-implicit-sender) passes with parallel collections enabledAkka.TestKit.Xunit.Testssuite: 28/28 pass