Skip to content

[v1.5] Cherry-pick #8182: wrap outer SynchronizationContext in ActorCellKeepingSynchronizationContext#8200

Merged
Aaronontheweb merged 1 commit into
akkadotnet:v1.5from
Aaronontheweb:cherry-pick/8182-v1.5
May 8, 2026
Merged

[v1.5] Cherry-pick #8182: wrap outer SynchronizationContext in ActorCellKeepingSynchronizationContext#8200
Aaronontheweb merged 1 commit into
akkadotnet:v1.5from
Aaronontheweb:cherry-pick/8182-v1.5

Conversation

@Aaronontheweb
Copy link
Copy Markdown
Member

Cherry-pick of #8182 from dev to v1.5.

Summary

  • Fixes xUnit v3 parallel-class implicit-sender leak in Akka.TestKit.Xunit where reused worker threads caused sibling tests to read the wrong InternalCurrentActorCellKeeper.Current value as their implicit sender.
  • Adds AkkaCleanAmbientContextAttribute (a BeforeAfterTestAttribute) that pins the running test's TestActor cell via AsyncLocal and installs ActorCellKeepingSynchronizationContext to wrap (not replace) the outer SC, preserving xUnit v3's MaxConcurrencySyncContext scheduling that downstream consumers like Akka.Hosting.TestKit depend on.
  • ActorCellKeepingSynchronizationContext now accepts an optional inner SynchronizationContext and delegates Post/Send to it while still wrapping callbacks with the cell-pinning save/restore window.
  • Adds regression tests: ParallelAmbientContextSpec and ActorCellKeepingSynchronizationContextSpec.
  • See fix: resolve implicit-sender leak under xUnit v3 parallel execution Akka.Hosting#735 and Akka.Hosting.TestKit implicit-sender leaks across ActorSystems under xUnit v3 parallel class execution Akka.Hosting#733.

Conflict resolution

The two CoreAPISpec.ApproveTestKit.*.verified.txt files conflicted because v1.5's API approval baseline uses the long-form attribute names (AssemblyMetadataAttribute, ComVisibleAttribute, etc.) while dev uses the short form. Kept v1.5's existing style and only added the new [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute("Akka.TestKit.Xunit")] line in alphabetical position. API approval tests pass locally on net10.0.

Test plan

  • Build Akka.TestKit.Xunit succeeds (Release)
  • Akka.API.Tests ApproveTestKit specs pass on net10.0
  • CI green on v1.5

…tionContext (akkadotnet#8182)

* [xUnit 3] Fix parallel-class implicit-sender leak in Akka.TestKit.Xunit

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.

* Update API approval list

* Keep xUnit ambient context plumbing internal

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.

* Remove exception-driven ambient context checks

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.

* fix: wrap outer SynchronizationContext instead of replacing it

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.

* fix: use AsyncLocal instead of ThreadStatic in BeforeAfterTestAttribute

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.

* fix: harden xUnit ambient context reuse

* refactor: convert AmbientContextState to record

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.

---------

Co-authored-by: Gregorius Soedharmo <arkatufus@yahoo.com>
(cherry picked from commit 515a266)
@Aaronontheweb Aaronontheweb merged commit e608054 into akkadotnet:v1.5 May 8, 2026
7 of 11 checks passed
@Aaronontheweb Aaronontheweb deleted the cherry-pick/8182-v1.5 branch May 8, 2026 01:38
@Aaronontheweb Aaronontheweb added this to the 1.5.68 milestone May 15, 2026
@Aaronontheweb Aaronontheweb added akka.net v1.5 Issues affecting Akka.NET v1.5 akka-testkit Akka.NET Testkit issues labels May 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

akka.net v1.5 Issues affecting Akka.NET v1.5 akka-testkit Akka.NET Testkit issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant