Skip to content

AOT (2/7): built-in mailbox types, mailbox requirement map, MailboxType/Mailboxes trimming annotations - #8602

Merged
Aaronontheweb merged 3 commits into
devfrom
aot/m1-b-mailboxes
Sep 24, 2026
Merged

Aaronontheweb merged 3 commits into
devfrom
aot/m1-b-mailboxes

Conversation

@Aaronontheweb

Copy link
Copy Markdown
Member

Changes

Second PR in the milestone-1 AOT stack. Base: aot/m1-a-feature-switch-and-first-tables.

Mailboxes was the first site the Native AOT canary died in once the feature switch and the
first three tables were in place. It read two kinds of HOCON type name through Type.GetType,
and both name types core itself ships:

  • the mailbox-type of akka.actor.default-mailbox and of every id under akka.actor.mailbox
  • the message queue semantics interfaces that are the keys of akka.actor.mailbox.requirements
    and the values a dispatcher's mailbox-requirement can take

The trimmer could not tell which type was loaded, so it removed them. The unrooted publish died
in Mailboxes.LookupConfigurator with ArgumentException: Cannot instantiate MailboxType Akka.Dispatch.UnboundedMailbox, defined in [akka.actor.default-mailbox], whose inner
MissingMethodException was the trimmed (Settings, Config) constructor. On the way there it
logged six Mailbox Requirement mapping [...] is not an actual type warnings, one per trimmed
interface.

What changed

Commit 1 — tables. Two BuiltIn* tables now sit in front of those sites, in the four-arm
shape the rest of the milestone uses: table → else if (AkkaFeatures.IsDynamicTypeLoadingSupported)
reflection in a [RequiresUnreferencedCode] method → else throw AkkaFeatures.NotBuiltIn(setting, value, alternative).

  • BuiltInMailboxTypes — a factory delegate per built-in MailboxType: UnboundedMailbox,
    BoundedMailbox, UnboundedDequeBasedMailbox, BoundedDequeBasedMailbox, LoggerMailboxType.
  • BuiltInMessageQueueSemantics — the seven built-in message queue semantics interfaces.

Each entry carries two spellings — the bare name akka.conf ships and the "Ns.T, Akka" form
HOCON in the wild also carries — and every lookup runs its value through
Akka.Util.TypeExtensions.StripAssemblyIdentity first, so the full versioned
AssemblyQualifiedName Akka.Hosting writes matches the second key whatever Version, Culture
or PublicKeyToken it names. A versioned third key would only ever match the current build, which
is why there isn't one; the Version=99.0.0.0 row in the AQN test is there to keep it that way.

Commit 2 — annotations. MailboxType gets a class-level
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)], and so do the Type
parameters of Mailboxes.HasRequiredType, Mailboxes.ProducesMessageQueue and
Mailboxes.GetRequiredType. Without them the trimmer dropped the interface lists those methods
read, and the canary failed with ArgumentException: No IProducesMessageQueue<TQueue> supplied for Akka.Event.LoggerMailboxType while starting DefaultLogger. Attributes only — no signature a
compiler binds to moves — but they show up in the API approval, so
CoreAPISpec.ApproveCore.DotNet.verified.txt is re-approved in the same commit.

What did NOT change: values and keys are not trimmed

An earlier revision of this branch trimmed both. That was wrong twice over:

  • Akka's HOCON GetString already trims values, across every syntax. The ?.Trim() calls on
    mailbox-type and mailbox-requirement were dead code, so they are gone and dev's
    IsNullOrEmpty checks are back verbatim.
  • HOCON does not trim keys, and trimming the akka.actor.mailbox.requirements keys was an
    outright regression: "Akka.Dispatch.IUnboundedMessageQueueSemantics " (one trailing space)
    would map onto the same Type as akka.conf's unpadded key, and
    _mailboxBindings.Add threw ArgumentException: An item with the same key has already been added out of ActorSystem.Create with the switch on, where dev warned and skipped.
    Measured, and now pinned by
    Should_warn_and_skip_an_unresolvable_requirement_key_When_dynamic_type_loading_is_enabled,
    which fails with exactly that exception if the .Trim() comes back.

The requirements key is therefore used verbatim everywhere except the table lookup, which sees only
StripAssemblyIdentity(key) — that strips assembly identity components, never whitespace, so the
padded key still falls through to Type.GetType, returns null, and is warned about and skipped as
dev did.

Behavior

With the switch on — the default, and what every existing application gets — every input
resolves to the same mailbox as before, with one difference in exception shape: a built-in
mailbox-type is constructed directly, so when its constructor rejects its config the
ArgumentException carries that constructor's own exception as InnerException rather than the
TargetInvocationException Activator.CreateInstance wrapped it in, and names the configured
spelling instead of Type.ToString(). Both arms share one
CannotInstantiate(name, id, inner) builder, so the message text itself is dev's. Relatedly, the
non-zero-push-timeout warning now sits outside that try, so an exception from a custom
IProducesPushTimeoutSemanticsMailbox.PushTimeout getter propagates as itself.

With the switch off, only the names in the tables resolve. The one behavior change beyond
exception types: an unresolvable key under akka.actor.mailbox.requirements throws out of
ActorSystem.Create rather than warning and skipping. That is the point — a binding silently
missing under AOT is worse than a startup failure that names the key.

Both recorded in BREAKING_CHANGES_V1.6.md.

Tests

src/core/Akka.Tests/Dispatch/MailboxFeatureSwitchSpec.cs, in A's DynamicTypeLoadingCollection
so it never runs beside another spec that flips the process-wide AppContext switch:

Test Guards
every mailbox id core ships resolves, switch off the tables cover a default boot (one system, one switch-off window)
a full versioned AQN mailbox-type resolves, switch off the StripAssemblyIdentity normalization — the Akka.Hosting case, plus a Version=99.0.0.0 row
a custom mailbox-type resolves, switch on the reflection fallback still works (switch-ON regression guard)
a custom mailbox-type is refused, switch off the throw arm, and that the message names <id>.mailbox-type, the value and the switch
an unknown akka.actor.mailbox.requirements key is refused, switch off the new hard failure, out of ActorSystem.Create
a padded built-in requirements key warns and is skipped, switch on keys are NOT trimmed (fails with the collision ArgumentException if they are)

The unbounded / bounded ids in the first test are not config ids — they are
LookupConfigurator's two shortcut arms, which this change does not touch. They are in the list
so a later refactor of those arms cannot quietly break the switch-off path.

Verification

  • dotnet build src/core/Akka/Akka.csproj -c Release -warnaserror → 0/0
  • dotnet build Akka.slnx -c Release → 0/0
  • dotnet test src/core/Akka.Tests --framework net10.0 → full suite green
  • dotnet test src/core/Akka.API.Tests → green; the approval diff is exactly the four
    [DynamicallyAccessedMembers] additions
  • AOT canary (src/aot/Akka.AOT.App, per its README): gets past mailboxes — no requirement-mapping
    warnings, no Cannot instantiate MailboxType — and now dies at LoggingBus.StartDefaultLoggers
    with Logger specified in config cannot be found: "Akka.Event.DefaultLogger". That is PR C.
    With -p:RootAkka=true (which preserves all of Akka.dll and so is not the pass/fail run) the
    canary reaches [canary] OK.

Net trim-warning effect on core: Mailboxes.cs loses IL2057, IL2070 and IL2075; it gains
IL2072 ×2, and ActorCell.cs gains IL2072 ×2. All four trace to Props.Type.get being
unannotated, which PR C closes.

Known, out of scope

src/core/Akka.Streams.TestKit/reference.conf:5 sets a custom
akka.actor.default-mailbox.mailbox-type, so the Streams TestKit cannot boot with the switch off.
Expected and fine — the TestKit is never published AOT.

Stack: PR 2 of 7 for AOT milestone 1 (bare local ActorSystem boots and exits 0 under Native AOT). Base is PR #8601 (A); this PR shows only its own two commits. Design and measurements: epic #7246.

Checklist

@Aaronontheweb Aaronontheweb added the AOT Ahead-of-Time (AOT) Compilation label Sep 23, 2026
@Aaronontheweb
Aaronontheweb added this pull request to stack #8607 September 23, 2026 02:07
@Aaronontheweb Aaronontheweb added this to the 1.6.0 milestone Sep 23, 2026
@Aaronontheweb
Aaronontheweb deleted the aot/m1-b-mailboxes branch September 23, 2026 02:24
@Aaronontheweb
Aaronontheweb restored the aot/m1-b-mailboxes branch September 23, 2026 02:41
@Aaronontheweb Aaronontheweb reopened this Sep 23, 2026
@Aaronontheweb
Aaronontheweb force-pushed the aot/m1-b-mailboxes branch 2 times, most recently from 8e3292b to 76e0513 Compare September 23, 2026 14:19
Base automatically changed from feature/aot-m1-a-feature-switch-and-first-tables to dev September 23, 2026 18:09
@Aaronontheweb
Aaronontheweb force-pushed the aot/m1-b-mailboxes branch 2 times, most recently from fa82f3f to cc13098 Compare September 23, 2026 21:13
…without reflection

Mailboxes read two kinds of HOCON type name through Type.GetType: the mailbox-type of every id
under akka.actor.mailbox (plus akka.actor.default-mailbox), and the message queue semantics
interfaces that are the keys of akka.actor.mailbox.requirements and the values a dispatcher's
mailbox-requirement can take. All of those names are types core itself ships, so the trimmer had
no way to tell which type was loaded and dropped it - the first unrooted Native AOT publish died
in Mailboxes.LookupConfigurator with ArgumentException "Cannot instantiate MailboxType
Akka.Dispatch.UnboundedMailbox, defined in [akka.actor.default-mailbox]", whose inner
MissingMethodException was the trimmed (Settings, Config) constructor.

Two tables now sit in front of those sites. BuiltInMailboxTypes holds a factory delegate per
built-in MailboxType - UnboundedMailbox, BoundedMailbox, UnboundedDequeBasedMailbox,
BoundedDequeBasedMailbox and LoggerMailboxType - and BuiltInMessageQueueSemantics maps the seven
built-in message queue semantics interfaces. A name that misses its table falls back to the old
reflection code, moved into a private static method marked [RequiresUnreferencedCode], and that
fallback only runs while dynamic type loading is on; with the switch off the site throws a
ConfigurationException built by AkkaFeatures.NotBuiltIn, so the wording matches every other
converted site.

Each table carries three spellings per type: akka.conf ships the bare name, HOCON in the wild also
carries the "Ns.T, Akka" form, and typeof(T).AssemblyQualifiedName adds the version/culture/
public-key form that Akka.Hosting writes. typeof is a constant to the trimmer and to ILC, so the
third spelling costs nothing and needs no string parsing. Each HOCON value is trimmed once, so the
table lookup, the reflection fallback and the exception message all work off the same string.

Behavior with the switch on is unchanged for every input, with three narrow exceptions: a built-in
mailbox type is now constructed directly, so a constructor that rejects its config reports its own
exception instead of the TargetInvocationException Activator.CreateInstance wrapped it in; a padded
value now resolves where Type.GetType rejected it; and a whitespace-only value now counts as blank
rather than as a type name. With the switch off, an unresolvable key under
akka.actor.mailbox.requirements now throws rather than logging a warning and skipping that binding,
which is the only switch-off difference that is more than an exception type. Recorded in
BREAKING_CHANGES_V1.6.md.

MailboxFeatureSwitchSpec covers every mailbox id core resolves on a default boot with the switch
off, both non-literal spellings of a built-in mailbox-type, and a mailbox-type living outside
Akka.dll with the switch both on and off.
…boundaries

Mailboxes decides which message queue an actor gets by reading interface lists off Types that
arrive from somewhere else: HasRequiredType and GetRequiredType look for IRequiresMessageQueue<T>
on an actor type handed in by Props, and ProducesMessageQueue and GetProducedMessageQueueType look
for IProducesMessageQueue<TQueue> on a MailboxType. The trimmer cannot see back to where those
Types came from, so it trimmed the interface lists and left the lookups finding nothing - the
unrooted Native AOT canary failed with ArgumentException "No IProducesMessageQueue<TQueue> supplied
for Akka.Event.LoggerMailboxType" while starting the DefaultLogger actor.

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] on the three Type
parameters, and on MailboxType at the class level so every subclass inherits it, is what tells the
trimmer to keep those interfaces.

This is an attributes-only change to the public surface - no signature a compiler binds to moves -
but the attributes do show up in the API approval, so CoreAPISpec.ApproveCore is re-approved here.
The one consequence for callers is at their own call site: a caller with trim analysis enabled that
passes an unannotated Type into these three methods now gets IL2072 and has to annotate the Type it
flows from, or pass a typeof(...).
…raft, and fix B's own regressions

BREAKING_CHANGES_V1.6.md: B's diff had replaced A's row with an
earlier, superseded draft of that same row (the "three spellings"
/ "typeof(T).AssemblyQualifiedName" / "trimmed once before it is
used" text). Restore the row exactly as it reads on
aot/m1-a-feature-switch-and-first-tables.

Mailboxes.GetMailboxRequirement: revert the
string.IsNullOrEmpty(mailboxRequirement) check back to
mailboxRequirement == null, matching dev parity - an empty
mailbox-requirement string must keep throwing (switch on via
Type.GetType("", true), switch off via NotBuiltIn), not silently
fall back to IMessageQueue.

MailboxFeatureSwitchSpec: the two exception-message assertions
that checked for DynamicTypeLoadingCollection.Name (the xunit
collection name, not the AppContext switch name) now check
AkkaFeaturesSpec.SwitchName instead.

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

LGTM

/// list off whichever <see cref="MailboxType"/> they are handed, so trimming has to keep
/// <see cref="IProducesMessageQueue{TQueue}"/> on every subclass.
/// </remarks>
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

LGTM

// A value that still misses the table falls through to the reflection path, which is unavailable (and
// therefore throws) once dynamic type loading is switched off. Do not remove a spelling, and do not
// add a versioned third key.
private static readonly Dictionary<string, Func<Settings, Config, MailboxType>> BuiltInMailboxTypes =

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

LGTM - exactly what I envisioned nearly two years ago when we started this journey, for solving "built in loading" AOT problems

{
type = builtIn;
}
else if (AkkaFeatures.IsDynamicTypeLoadingSupported)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

LGTM - the "non AOT" path is enabled here

/// </param>
/// <returns><c>true</c> if this actor has a message queue type requirement. <c>false</c> otherwise.</returns>
public bool HasRequiredType(Type actorType)
public bool HasRequiredType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type actorType)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

LGTM

{
throw new ArgumentException($"Cannot instantiate MailboxType {mailboxType}, defined in [{id}]. Make sure it has a public " +
"constructor with [Akka.Actor.Settings, Akka.Configuration.Config] parameters", ex);
configurator = CreateMailboxType(mailboxTypeName, id, Settings, conf);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

instantiate a dynamic mailbox, when allowed

/// The required message queue type, or <c>null</c> when <paramref name="actorType"/> does not implement
/// <see cref="IRequiresMessageQueue{T}"/>.
/// </returns>
public Type GetRequiredType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type actorType)

Copy link
Copy Markdown
Member Author

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 e88b4f1 into dev Sep 24, 2026
15 checks passed
@Aaronontheweb
Aaronontheweb deleted the aot/m1-b-mailboxes branch September 24, 2026 03:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AOT Ahead-of-Time (AOT) Compilation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant