Skip to content

Widen ActorTaskScheduler(ActorCell) ctor to protected internal - #8158

Merged
Aaronontheweb merged 1 commit into
akkadotnet:devfrom
Aaronontheweb:fix/actor-task-scheduler-protected-ctor
Apr 13, 2026
Merged

Widen ActorTaskScheduler(ActorCell) ctor to protected internal#8158
Aaronontheweb merged 1 commit into
akkadotnet:devfrom
Aaronontheweb:fix/actor-task-scheduler-protected-ctor

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Member

Summary

Widen the ActorTaskScheduler(ActorCell) constructor from internal to protected internal, so external libraries can subclass ActorTaskScheduler and override the existing OnBeforeTaskStarted / OnAfterTaskCompleted hooks without reflection or runtime codegen.

No behavior change — this is a pure access widening. The internal half of protected internal preserves the existing new ActorTaskScheduler(this) call site inside ActorCell, so no other source in Akka.dll had to move.

Why

OnBeforeTaskStarted / OnAfterTaskCompleted have always been protected virtual. They're clearly intended as an extension point — the whole reason for their existence is to let subclasses observe the async handler lifecycle inside ActorTaskScheduler.RunTask. But the base constructor is internal, so in practice no assembly outside Akka.dll can actually subclass and wire them up. The extension point has been half-exposed ever since it was added.

Motivation (Phobos)

This blocks a real fix in Phobos (the Akka.NET observability plugin from Petabridge).

Phobos's PhobosActorCell opens an akka.msg.recv span at the start of each message and disposes it synchronously in a finally block when base.ReceiveMessage returns. For ReceiveAsync<T> / CommandAsync<T> handlers, base.ReceiveMessage returns at the first await yield point — ActorTaskScheduler.RunTask has suspended the mailbox and scheduled the continuation, but the user's Task is still running. Phobos closes the span immediately. All post-await work (Task.WhenAll, HTTP calls, external validation, nested Asks) is invisible in traces, and downstream customer-facing spans show durations of ~1 ms for handlers that actually take 100+ ms.

The correct fix is for Phobos to subclass ActorTaskScheduler, override OnBeforeTaskStarted to capture the span, override OnAfterTaskCompleted to dispose it once the Task truly completes, and expose the custom scheduler via a PhobosActorCell.TaskScheduler override. Clean, zero-reflection, runs on every TFM.

Today that's blocked on the internal ctor. Alternatives we looked at and rejected:

  • IgnoresAccessChecksToGenerator — compile-time trick that widens the reference assembly's view and ships [assembly: IgnoresAccessChecksTo("Akka")] in Phobos. Works beautifully on CoreCLR, but IgnoresAccessChecksTo is not honored by the .NET Framework JIT. Phobos has .NET Framework customers, and the type load would throw MethodAccessException on first use — worse than the bug we're fixing.
  • Reflection / UnsafeAccessor — neither lets a derived class chain to an inaccessible base ctor. The derived class's IL needs a valid call base::.ctor(...) at compile time.
  • Runtime Reflection.Emit subclass — technically cross-runtime but adds codegen surface area, cold-start cost, and is AOT-hostile.
  • Deferring span close until the next message — works but gives wrong end-times during idle periods and needs a bunch of bookkeeping to handle stop/restart.

Widening one keyword here is by a wide margin the cleanest option, and it's the right API shape regardless of Phobos — the hooks deserved to be genuinely usable from outside the assembly the whole time.

Backport

This needs to ship on both branches:

  • dev (target of this PR) → 1.6.x line
  • v1.5 — a trivial cherry-pick; Phobos 2.11.x depends on Akka 1.5.x and that's where the customer-facing fix lands. Happy to open the backport PR myself once this merges.

Test plan

  • dotnet build src/core/Akka/Akka.csproj -c Release — clean
  • dotnet test src/core/Akka.API.Tests/Akka.API.Tests.csproj --filter "FullyQualifiedName~CoreAPISpec" — 15/15 pass, including the updated ApproveCore snapshots
  • API verify snapshots updated for both .Net and .DotNet variants (public API generator reports protected internal ctors as protected in the snapshot view)

The two virtual hooks OnBeforeTaskStarted / OnAfterTaskCompleted have
always been protected, clearly intended as extension points for
libraries that need to observe async message handler lifetimes. But the
ctor was internal, blocking any external subclass from actually hooking
them without reflection or runtime codegen.

Widening the ctor to protected internal finishes the extension point
that was already half-exposed. protected lets external libraries
subclass; the internal part preserves the existing new
ActorTaskScheduler(this) call site in ActorCell without requiring a
derived type there.

Motivation: Phobos (the Akka.NET observability plugin) needs to
subclass ActorTaskScheduler to keep akka.msg.recv spans open for the
full duration of ReceiveAsync / CommandAsync handlers. Today the span
closes at the first await yield point, hiding all post-await work from
traces. Customers running Phobos on both .NET and .NET Framework hit
this, and we need a cross-runtime fix — IgnoresAccessChecksTo works
only on CoreCLR, reflection can't synthesize a subclass with an
inaccessible base ctor, and runtime codegen is AOT-hostile. Widening
the ctor is the clean fix.

No behavior change. Pure access widening, no caller breaks.

@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 merged commit fe2b44b into akkadotnet:dev Apr 13, 2026
9 of 12 checks passed
@Aaronontheweb
Aaronontheweb deleted the fix/actor-task-scheduler-protected-ctor branch April 13, 2026 17:03
@Aaronontheweb Aaronontheweb added this to the 1.5.66 milestone Apr 22, 2026
Aaronontheweb added a commit to Aaronontheweb/akka.net that referenced this pull request Apr 24, 2026
… surface

Apply the same API changes introduced by cherry-picks to the .NET Framework
API approval files:
- ActorTaskScheduler protected ctor (akkadotnet#8158)
- MemoryJournal: Messages→Storage (JournalStorage), remove Update, Read param rename (akkadotnet#8184)
- SharedMemoryJournal: Messages→Storage
- MemorySnapshotStore: Snapshots→Storage (SnapshotStorage nested class) (akkadotnet#8184)
- SnapshotEntry: sealed with readonly constructor-initialized properties (akkadotnet#8184)
- StreamsDiagnostics class in Akka.Streams.Implementation (akkadotnet#8160)
Aaronontheweb added a commit that referenced this pull request Apr 24, 2026
The two virtual hooks OnBeforeTaskStarted / OnAfterTaskCompleted have
always been protected, clearly intended as extension points for
libraries that need to observe async message handler lifetimes. But the
ctor was internal, blocking any external subclass from actually hooking
them without reflection or runtime codegen.

Widening the ctor to protected internal finishes the extension point
that was already half-exposed. protected lets external libraries
subclass; the internal part preserves the existing new
ActorTaskScheduler(this) call site in ActorCell without requiring a
derived type there.

Motivation: Phobos (the Akka.NET observability plugin) needs to
subclass ActorTaskScheduler to keep akka.msg.recv spans open for the
full duration of ReceiveAsync / CommandAsync handlers. Today the span
closes at the first await yield point, hiding all post-await work from
traces. Customers running Phobos on both .NET and .NET Framework hit
this, and we need a cross-runtime fix — IgnoresAccessChecksTo works
only on CoreCLR, reflection can't synthesize a subclass with an
inaccessible base ctor, and runtime codegen is AOT-hostile. Widening
the ctor is the clean fix.

No behavior change. Pure access widening, no caller breaks.
Aaronontheweb added a commit that referenced this pull request Apr 24, 2026
… surface

Apply the same API changes introduced by cherry-picks to the .NET Framework
API approval files:
- ActorTaskScheduler protected ctor (#8158)
- MemoryJournal: Messages→Storage (JournalStorage), remove Update, Read param rename (#8184)
- SharedMemoryJournal: Messages→Storage
- MemorySnapshotStore: Snapshots→Storage (SnapshotStorage nested class) (#8184)
- SnapshotEntry: sealed with readonly constructor-initialized properties (#8184)
- StreamsDiagnostics class in Akka.Streams.Implementation (#8160)
This was referenced Apr 27, 2026
This was referenced Jun 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants