You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Test framework: xUnit 2.x (xunit 2.x / xunit.runner.visualstudio)
TFM: net8.0 / net10.0
Describe the bug
Akka.Hosting.TestKit sets Akka.Actor.Internal.InternalCurrentActorCellKeeper.Current = TestActor.Underlying (via EnsureImplicitSender / test init) so that inside a test body the single-argument actor.Tell(msg) picks up TestActor as the implicit sender. That field is [ThreadStatic] and does not flow through ExecutionContext.
#733 reported that under parallel class execution this leaks across ActorSystems — an await continuation resumes on a ThreadPool thread another test pinned, so Tell() uses the wrong implicit sender and replies cross ActorSystem boundaries. #735 fixed it for the xUnit v3 package (Akka.Hosting.TestKit) by adding a wrapping ActorCellKeepingSynchronizationContext plus a BeforeAfterTestAttribute (HostingCleanAmbientContext) that saves/pins/restores Current around continuations and cleans it up in After().
That fix was never ported to Akka.Hosting.TestKit.Xunit2. PR #735 explicitly notes the xUnit2 variant as "unaffected," and it can be confirmed directly in the shipped 1.5.70 assemblies:
Akka.Hosting.TestKit.dll (v3) — contains AkkaCleanAmbientContextAttribute (the cleanup). ✅
Akka.Hosting.TestKit.Xunit2.dll — contains EnsureImplicitSender (the seeding) but none of the cleanup types (ActorCellKeepingSynchronizationContext / HostingCleanAmbientContext / AkkaCleanAmbientContext). ❌
xUnit2 still runs test collections in parallel by default, so the original #733 leak is fully live for xUnit2 consumers.
Broader: because the seeded cell is never cleared and Mailbox.Run restores Current to its previous value (not null), a contaminated ThreadPool thread stays contaminated for the life of the process. Any later work item on that physical thread that reads ActorCell.Current — including non-TestKit code sharing the process ThreadPool (e.g. a WebApplicationFactory/TestServer request handler, or any .Ask<T>() issued from pooled code) — observes a stale actor cell from a finished test's /system/testActor. Code that branches on "am I currently inside an actor?" then does the wrong thing on a thread that is not, in fact, running an actor.
To Reproduce
Two Akka.Hosting.TestKit.Xunit2.TestKit-derived specs in one assembly (distinct classes → separate collections → run in parallel), each awaiting before relying on the implicit sender:
usingSystem;usingSystem.Threading.Tasks;usingAkka.Actor;usingAkka.Actor.Dsl;usingAkka.Hosting;usingXunit;publicabstractclassImplicitSenderReproBase:Akka.Hosting.TestKit.TestKit{protectedImplicitSenderReproBase():base(actorSystemName:"repro-"+Guid.NewGuid().ToString("N")){}protectedoverridevoidConfigureAkka(AkkaConfigurationBuilderbuilder,IServiceProviderprovider){}protectedasyncTaskImplicitSenderShouldBeOwnTestActor(){varecho=Sys.ActorOf(act =>act.ReceiveAny((_,ctx)=>ctx.Sender.Tell("reply")));// force a continuation onto a pool thread a sibling test may have pinnedawaitTask.Yield();awaitTask.Delay(Random.Shared.Next(5,30));echo.Tell("ping");// relies on implicit sender == this test's TestActorawaitExpectMsgAsync("reply");// intermittently times out: reply crossed to a sibling ActorSystem}[Fact]publicTaskTest1()=>ImplicitSenderShouldBeOwnTestActor();[Fact]publicTaskTest2()=>ImplicitSenderShouldBeOwnTestActor();}publicclassImplicitSenderRepro01:ImplicitSenderReproBase{}publicclassImplicitSenderRepro02:ImplicitSenderReproBase{}publicclassImplicitSenderRepro03:ImplicitSenderReproBase{}publicclassImplicitSenderRepro04:ImplicitSenderReproBase{}
Run repeatedly under load (or on a low-core agent). The failure rate rises with ThreadPool contention and drops to ~0 with parallelization disabled — the signature of a [ThreadStatic] shared-pool leak. The same specs pass on the xUnit v3Akka.Hosting.TestKit (which has the #735 cleanup).
Reuse the (xUnit-agnostic) wrapping ActorCellKeepingSynchronizationContext.
Add an xUnit2 BeforeAfterTestAttribute equivalent to HostingCleanAmbientContext that installs the decorator, pins the cell for the test, and restores/clears Current in After() so no seeded cell survives onto a pooled thread.
Keep the EnsureImplicitSenderCurrent == null guard consistent across both variants.
Add parallel-execution regression tests mirroring ParallelAmbientContextSpec for the xUnit2 package.
Defense-in-depth worth considering: guarantee Current is cleared at test teardown regardless of the SC path, so a leaked cell can never outlive the test onto a thread reused by unrelated code.
Version Information
Akka.Hosting.TestKitimplicit-sender leaks acrossActorSystems under xUnit v3 parallel class execution #733)xunit2.x /xunit.runner.visualstudio)Describe the bug
Akka.Hosting.TestKitsetsAkka.Actor.Internal.InternalCurrentActorCellKeeper.Current = TestActor.Underlying(viaEnsureImplicitSender/ test init) so that inside a test body the single-argumentactor.Tell(msg)picks upTestActoras the implicit sender. That field is[ThreadStatic]and does not flow throughExecutionContext.#733 reported that under parallel class execution this leaks across
ActorSystems — anawaitcontinuation resumes on a ThreadPool thread another test pinned, soTell()uses the wrong implicit sender and replies crossActorSystemboundaries. #735 fixed it for the xUnit v3 package (Akka.Hosting.TestKit) by adding a wrappingActorCellKeepingSynchronizationContextplus aBeforeAfterTestAttribute(HostingCleanAmbientContext) that saves/pins/restoresCurrentaround continuations and cleans it up inAfter().That fix was never ported to
Akka.Hosting.TestKit.Xunit2. PR #735 explicitly notes the xUnit2 variant as "unaffected," and it can be confirmed directly in the shipped 1.5.70 assemblies:Akka.Hosting.TestKit.dll(v3) — containsAkkaCleanAmbientContextAttribute(the cleanup). ✅Akka.Hosting.TestKit.Xunit2.dll— containsEnsureImplicitSender(the seeding) but none of the cleanup types (ActorCellKeepingSynchronizationContext/HostingCleanAmbientContext/AkkaCleanAmbientContext). ❌xUnit2 still runs test collections in parallel by default, so the original #733 leak is fully live for xUnit2 consumers.
Two consequences
Akka.Hosting.TestKitimplicit-sender leaks acrossActorSystems under xUnit v3 parallel class execution #733, in xUnit2: intermittentExpectMsg<T>timeouts (a test's reply went to a sibling'sTestActor) andExpectNoMsg/ExpectTerminatedfailures (a sibling receives a stray reply from a foreignActorSystem).Mailbox.RunrestoresCurrentto its previous value (notnull), a contaminated ThreadPool thread stays contaminated for the life of the process. Any later work item on that physical thread that readsActorCell.Current— including non-TestKit code sharing the process ThreadPool (e.g. aWebApplicationFactory/TestServerrequest handler, or any.Ask<T>()issued from pooled code) — observes a stale actor cell from a finished test's/system/testActor. Code that branches on "am I currently inside an actor?" then does the wrong thing on a thread that is not, in fact, running an actor.To Reproduce
Two
Akka.Hosting.TestKit.Xunit2.TestKit-derived specs in one assembly (distinct classes → separate collections → run in parallel), each awaiting before relying on the implicit sender:Run repeatedly under load (or on a low-core agent). The failure rate rises with ThreadPool contention and drops to ~0 with parallelization disabled — the signature of a
[ThreadStatic]shared-pool leak. The same specs pass on the xUnit v3Akka.Hosting.TestKit(which has the #735 cleanup).Proposed fix
Port #735's approach to the xUnit2 variant:
ActorCellKeepingSynchronizationContext.BeforeAfterTestAttributeequivalent toHostingCleanAmbientContextthat installs the decorator, pins the cell for the test, and restores/clearsCurrentinAfter()so no seeded cell survives onto a pooled thread.EnsureImplicitSenderCurrent == nullguard consistent across both variants.ParallelAmbientContextSpecfor the xUnit2 package.Defense-in-depth worth considering: guarantee
Currentis cleared at test teardown regardless of the SC path, so a leaked cell can never outlive the test onto a thread reused by unrelated code.Related
Akka.Hosting.TestKitimplicit-sender leaks acrossActorSystems under xUnit v3 parallel class execution #733 (root cause, xUnit v3), fix: resolve implicit-sender leak under xUnit v3 parallel execution #735 (fix, xUnit v3 only), Akka.Hosting.TestKit:TestActoris no longer the implicit sender #237 (open: implicit-sender reliability in async mode).