Repository navigation
AOT (2/7): built-in mailbox types, mailbox requirement map, MailboxType/Mailboxes trimming annotations - #8602
Merged
Merged
Conversation
5 tasks done
Aaronontheweb
added this pull request to stack #8607
September 23, 2026 02:07
Aaronontheweb
force-pushed
the
aot/m1-b-mailboxes
branch
2 times, most recently
from
September 23, 2026 14:19
8e3292b to
76e0513
Compare
Aaronontheweb
force-pushed
the
aot/m1-b-mailboxes
branch
from
September 23, 2026 14:47
76e0513 to
9dd48cf
Compare
Base automatically changed from
feature/aot-m1-a-feature-switch-and-first-tables
to
dev
September 23, 2026 18:09
Aaronontheweb
force-pushed
the
aot/m1-b-mailboxes
branch
2 times, most recently
from
September 23, 2026 21:13
fa82f3f to
cc13098
Compare
…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
force-pushed
the
aot/m1-b-mailboxes
branch
from
September 24, 2026 02:05
cc13098 to
ba2667d
Compare
Aaronontheweb
commented
Sep 24, 2026
| /// 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)] |
| // 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 = |
Member
Author
There was a problem hiding this comment.
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) |
Member
Author
There was a problem hiding this comment.
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) |
| { | ||
| 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); |
Member
Author
There was a problem hiding this comment.
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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes
Second PR in the milestone-1 AOT stack. Base:
aot/m1-a-feature-switch-and-first-tables.Mailboxeswas the first site the Native AOT canary died in once the feature switch and thefirst three tables were in place. It read two kinds of HOCON type name through
Type.GetType,and both name types core itself ships:
mailbox-typeofakka.actor.default-mailboxand of every id underakka.actor.mailboxakka.actor.mailbox.requirementsand the values a dispatcher's
mailbox-requirementcan takeThe trimmer could not tell which type was loaded, so it removed them. The unrooted publish died
in
Mailboxes.LookupConfiguratorwithArgumentException: Cannot instantiate MailboxType Akka.Dispatch.UnboundedMailbox, defined in [akka.actor.default-mailbox], whose innerMissingMethodExceptionwas the trimmed(Settings, Config)constructor. On the way there itlogged six
Mailbox Requirement mapping [...] is not an actual typewarnings, one per trimmedinterface.
What changed
Commit 1 — tables. Two
BuiltIn*tables now sit in front of those sites, in the four-armshape 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-inMailboxType:UnboundedMailbox,BoundedMailbox,UnboundedDequeBasedMailbox,BoundedDequeBasedMailbox,LoggerMailboxType.BuiltInMessageQueueSemantics— the seven built-in message queue semantics interfaces.Each entry carries two spellings — the bare name
akka.confships and the"Ns.T, Akka"formHOCON in the wild also carries — and every lookup runs its value through
Akka.Util.TypeExtensions.StripAssemblyIdentityfirst, so the full versionedAssemblyQualifiedNameAkka.Hosting writes matches the second key whateverVersion,Cultureor
PublicKeyTokenit names. A versioned third key would only ever match the current build, whichis why there isn't one; the
Version=99.0.0.0row in the AQN test is there to keep it that way.Commit 2 — annotations.
MailboxTypegets a class-level[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)], and so do theTypeparameters of
Mailboxes.HasRequiredType,Mailboxes.ProducesMessageQueueandMailboxes.GetRequiredType. Without them the trimmer dropped the interface lists those methodsread, and the canary failed with
ArgumentException: No IProducesMessageQueue<TQueue> supplied for Akka.Event.LoggerMailboxTypewhile startingDefaultLogger. Attributes only — no signature acompiler binds to moves — but they show up in the API approval, so
CoreAPISpec.ApproveCore.DotNet.verified.txtis 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:
GetStringalready trims values, across every syntax. The?.Trim()calls onmailbox-typeandmailbox-requirementwere dead code, so they are gone and dev'sIsNullOrEmptychecks are back verbatim.akka.actor.mailbox.requirementskeys was anoutright regression:
"Akka.Dispatch.IUnboundedMessageQueueSemantics "(one trailing space)would map onto the same
Typeasakka.conf's unpadded key, and_mailboxBindings.AddthrewArgumentException: An item with the same key has already been addedout ofActorSystem.Createwith 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 thepadded key still falls through to
Type.GetType, returns null, and is warned about and skipped asdev 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-typeis constructed directly, so when its constructor rejects its config theArgumentExceptioncarries that constructor's own exception asInnerExceptionrather than theTargetInvocationExceptionActivator.CreateInstancewrapped it in, and names the configuredspelling instead of
Type.ToString(). Both arms share oneCannotInstantiate(name, id, inner)builder, so the message text itself is dev's. Relatedly, thenon-zero-push-timeout warning now sits outside that
try, so an exception from a customIProducesPushTimeoutSemanticsMailbox.PushTimeoutgetter 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.requirementsthrows out ofActorSystem.Createrather than warning and skipping. That is the point — a binding silentlymissing 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'sDynamicTypeLoadingCollectionso it never runs beside another spec that flips the process-wide
AppContextswitch:mailbox-typeresolves, switch offStripAssemblyIdentitynormalization — the Akka.Hosting case, plus aVersion=99.0.0.0rowmailbox-typeresolves, switch onmailbox-typeis refused, switch off<id>.mailbox-type, the value and the switchakka.actor.mailbox.requirementskey is refused, switch offActorSystem.CreateArgumentExceptionif they are)The
unbounded/boundedids in the first test are not config ids — they areLookupConfigurator's two shortcut arms, which this change does not touch. They are in the listso 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/0dotnet build Akka.slnx -c Release→ 0/0dotnet test src/core/Akka.Tests --framework net10.0→ full suite greendotnet test src/core/Akka.API.Tests→ green; the approval diff is exactly the four[DynamicallyAccessedMembers]additionssrc/aot/Akka.AOT.App, per its README): gets past mailboxes — no requirement-mappingwarnings, no
Cannot instantiate MailboxType— and now dies atLoggingBus.StartDefaultLoggerswith
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) thecanary reaches
[canary] OK.Net trim-warning effect on core:
Mailboxes.cslosesIL2057,IL2070andIL2075; it gainsIL2072×2, andActorCell.csgainsIL2072×2. All four trace toProps.Type.getbeingunannotated, which PR C closes.
Known, out of scope
src/core/Akka.Streams.TestKit/reference.conf:5sets a customakka.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
ActorSystemboots 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