Skip to content

fix: wrap outer SynchronizationContext in ActorCellKeepingSynchronizationContext#8182

Merged
Aaronontheweb merged 22 commits into
akkadotnet:devfrom
Aaronontheweb:fix/wrapping-sync-context
May 7, 2026
Merged

fix: wrap outer SynchronizationContext in ActorCellKeepingSynchronizationContext#8182
Aaronontheweb merged 22 commits into
akkadotnet:devfrom
Aaronontheweb:fix/wrapping-sync-context

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Member

Summary

Builds on #8174. Makes ActorCellKeepingSynchronizationContext a proper decorator that preserves the outer SynchronizationContext instead of replacing it with raw ThreadPool dispatch.

Problem: #8174's Post() dispatches via ThreadPool.QueueUserWorkItem, discarding whatever SC was active when the attribute installed it. This works for Akka.TestKit.Xunit tests but hangs downstream consumers like Akka.Hosting.TestKit whose async IHost lifecycle depends on xUnit v3's MaxConcurrencySyncContext scheduling. See akkadotnet/Akka.Hosting#733 and akkadotnet/Akka.Hosting#735 for the full investigation.

Fix (2 files, backward-compatible):

  • ActorCellKeepingSynchronizationContext — adds optional inner parameter (defaults to null). 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. Added CreateCopy() override to preserve the wrapping chain.
  • AkkaCleanAmbientContextAttribute — captures SynchronizationContext.Current in Before() and passes it as the inner SC. Restores the original SC in After(). Added _applied guard so After() is a no-op when Before() returned early (non-TestKitBase test class).

Depends on

Test plan

Arkatufus and others added 11 commits April 23, 2026 03:23
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.
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 Arkatufus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have a big question

Arkatufus and others added 2 commits April 24, 2026 22:49
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 Arkatufus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Aaronontheweb Aaronontheweb marked this pull request as ready for review April 24, 2026 19:44
@Aaronontheweb Aaronontheweb added the akka-testkit Akka.NET Testkit issues label Apr 24, 2026
Arkatufus
Arkatufus previously requested changes Apr 28, 2026

@Arkatufus Arkatufus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small nitpick

Arkatufus and others added 2 commits May 4, 2026 17:44
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.
@Aaronontheweb Aaronontheweb enabled auto-merge (squash) May 7, 2026 22:00
@Aaronontheweb Aaronontheweb dismissed Arkatufus’s stale review May 7, 2026 22:01

Implemented suggestions

This was referenced Jun 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

akka-testkit Akka.NET Testkit issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants