Repository navigation
AOT (3/7): Props/actor-type trimming annotations, akka.loggers from a built-in table, Akka.DependencyInjection ripple - #8603
Merged
Conversation
5 tasks done
Aaronontheweb
added this pull request to stack #8607
September 23, 2026 02:07
Aaronontheweb
force-pushed
the
aot/m1-c-props-and-loggers
branch
from
September 23, 2026 02:46
761fad5 to
5bb17d7
Compare
Aaronontheweb
force-pushed
the
aot/m1-c-props-and-loggers
branch
from
September 23, 2026 14:19
5bb17d7 to
6523341
Compare
Aaronontheweb
force-pushed
the
aot/m1-c-props-and-loggers
branch
from
September 23, 2026 14:47
6523341 to
8c8730f
Compare
Aaronontheweb
force-pushed
the
aot/m1-c-props-and-loggers
branch
from
September 23, 2026 18:09
8c8730f to
20e1ffe
Compare
Aaronontheweb
force-pushed
the
aot/m1-c-props-and-loggers
branch
from
September 23, 2026 21:13
20e1ffe to
1023140
Compare
Aaronontheweb
force-pushed
the
aot/m1-c-props-and-loggers
branch
from
September 24, 2026 02:05
1023140 to
830d6a0
Compare
Aaronontheweb
force-pushed
the
aot/m1-c-props-and-loggers
branch
from
September 24, 2026 03:31
830d6a0 to
8f01d80
Compare
This was referenced Sep 24, 2026
…flow Every actor type Akka.NET instantiates arrives through Props, and two things have to survive trimming once it does: the public constructor Activator.CreateInstance calls, and the IRequiresMessageQueue<> interfaces Mailboxes.HasRequiredType reads off the type to pick a mailbox. Neither was annotated, so the trimmer had no reason to keep either, and the unrooted Native AOT canary failed with MissingMethodException on Akka.Event.EventStreamUnsubscriber and on Akka.Actor.CoordinatedShutdownExtension, and with "No IProducesMessageQueue<TQueue> supplied for Akka.Event.LoggerMailboxType". Props now carries one named contract, Props.ActorTypeMembers (PublicConstructors | Interfaces), along the whole flow: the Type-taking Props constructors, Props.Type, Props.Create(Type, ...), the three Create<TActor> overloads, CreateBy<TProducer>, CreateProducer, ActivatorProducer, DefaultProducer, FactoryConsumer<>, DynamicProps<>, IIndirectActorProducer.ActorType, ActorRefFactoryExtensions.ActorOf<TActor>, and ExtendedActorSystem.SystemActorOf<TActor> with its ActorSystemImpl override. Using the same constant everywhere means the annotation flows from a caller's typeof(...) to the Activator call without a gap that trim analysis has to warn about. ActorSystemWithExtensions.WithExtension<T>(ActorSystem, Type) and WithExtension<T, TI> take the narrower PublicParameterlessConstructor instead. They are not on the Props path - the type they hand to Activator is an IExtensionId, there is no mailbox requirement to read off it, and both call the parameterless CreateInstance - so that is exactly what the trimmer needs to keep and exactly what the warnings on the base branch asked for. Measured against the base branch, this removes eight trim warnings, all of them an actor type or Props.Type losing its annotation at a call site: IL2072 at ActorCell.cs:243 and :245, at Mailboxes.cs:433 and :452 and at Props.cs:691, IL2067 at Extensions.cs:116 and Props.cs:588, and IL2087 at Extensions.cs:136. It adds three, all of them a Type that now flows into an annotated parameter from somewhere that is not annotated, and none of them on the local boot path. They only appear with -p:RootAkka=true, because the unrooted canary never reaches them: IL2072 at Props.cs:666, where PropsSurrogate hands its deserialized Type field to the Props constructor - that type comes off the wire, so there is nothing for the trimmer to root, and remote deployment already needs dynamic type loading - and IL2072 at IO/Dns.cs:275 and IO/SimpleDnsManager.cs:32, where IDnsProvider.ManagerClass/ActorClass reach Props.Create. The DNS pair goes away later in this stack, when those two actors are constructed directly instead of by name. Nothing about this changes at runtime. The attributes are only visible to trim analysis, so the approved public API surface grows the attributes and nothing else.
akka.loggers is the last type-name setting on the local boot path that still went straight through Type.GetType, so the trimmer could not tell that Akka.Event.DefaultLogger - the one logger core's own akka.conf names - was loaded, and dropped it. The unrooted Native AOT canary died in StartDefaultLoggers with ConfigurationException 'Logger specified in config cannot be found: "Akka.Event.DefaultLogger"'. StartDefaultLoggers now asks GetBuiltInLoggerType, which matches the configured name against the names of the three loggers Akka.dll ships - DefaultLogger, StandardOutLogger and TraceLogger, whose own documentation tells users to name it here - and returns the type from a constant typeof expression. A value that misses those tables falls back to the old Type.GetType, 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 it names the setting, the value and the switch in the same words as every other site. Each logger carries two spellings - the bare name and the "Ns.T, Akka" form - and the lookup runs the configured value through TypeExtensions.StripAssemblyIdentity first, so a full AssemblyQualifiedName matches the second key whatever version, culture or public key token it names. That matters for real input rather than in theory: Akka.Hosting writes the versioned form into HOCON, and a literal typeof(T).AssemblyQualifiedName key would only ever match the one build that compiled it, so HOCON written against any other Akka version would miss the table and, with the switch off, throw. The spec covers that case with a Version=99.0.0.0 spelling per logger. The lookup is a string[] matched inside one method with an annotated return, not the Dictionary<string, Type> the sibling tables use, because a dictionary cannot carry DynamicallyAccessedMembers on its values: the type coming back out of TryGetValue arrives unannotated, and the trimmer then drops the constructor Props needs. That is not theoretical - the first version of this commit used a dictionary, the publish reported IL2067 at the AddLogger call, and the canary started DefaultLogger only to get MissingMethodException 'No parameterless constructor defined for type Akka.Event.DefaultLogger'. The annotated return fixes both. The table hands back a Type rather than a factory because StartDefaultLoggers goes on to call Props.Create(loggerType) and start an actor; with the annotations from the previous commit that Type carries its constructor and interface metadata through to the Activator call. This is the ninth trim warning the branch removes: IL2057 at LoggingBus.cs:117 on the base branch. Behavior with the switch on is unchanged for every input: Akka's HOCON already trims a value in every syntax - quoted or bare, scalar or list element - so there is nothing left for this code to normalize beyond assembly identity, and it does not touch config keys. Recorded in BREAKING_CHANGES_V1.6.md.
IIndirectActorProducer.ActorType is annotated in core now, so every path that feeds it has to carry the same contract or trim analysis reports the gap at the boundary. The three IDependencyResolver.Props members, their implementations on DependencyResolver, ServiceProvider and ServiceProviderDependencyResolver, and ServiceProviderActorProducer (both the Type-taking constructor and the generic subclass) take Props.ActorTypeMembers, which Akka.DependencyInjection can name directly because core already grants it InternalsVisibleTo. ServiceProviderActorProducer.Produce builds the actor through ActivatorUtilities.CreateInstance, whose Type parameter asks for PublicConstructors - the half of Props.ActorTypeMembers that covers Activator - so the same constant satisfies it; the Interfaces half is what the mailbox requirement check needs afterwards. Annotation-only: no runtime behavior changes, and Akka.DependencyInjection has no API approval file, so there is no approved-surface churn either.
…tInLoggersSpec Should_accept_the_built_in_standard_out_logger_When_dynamic_type_loading_is_disabled asserted system.Settings.StdoutLogger.Should().BeOfType<StandardOutLogger>(), which is always true - StdoutLogger comes from akka.stdout-logger-class, not from the akka.loggers spelling under test. Remove that assertion; the test's real check is that ActorSystem.Create does not throw. Replace the local SwitchName const with AkkaFeaturesSpec.SwitchName.
Aaronontheweb
force-pushed
the
aot/m1-c-props-and-loggers
branch
from
September 24, 2026 13:58
8f01d80 to
d626066
Compare
Aaronontheweb
commented
Sep 24, 2026
| [Fact(DisplayName = "LoggingBus should resolve a logger named in HOCON when dynamic type loading is on")] | ||
| public async Task Should_resolve_a_custom_logger_When_dynamic_type_loading_is_enabled() | ||
| { | ||
| await AkkaFeaturesSpec.WithDynamicTypeLoading(true, async () => |
| } | ||
|
|
||
| [Fact(DisplayName = "LoggingBus should reject a logger named in HOCON that is not built in when dynamic type loading is off")] | ||
| public async Task Should_throw_ConfigurationException_When_the_logger_is_not_built_in_and_dynamic_type_loading_is_disabled() |
| /// </exception> | ||
| /// <returns>A reference to the underlying actor.</returns> | ||
| public override IActorRef SystemActorOf<TActor>(string name = null) | ||
| public override IActorRef SystemActorOf<[DynamicallyAccessedMembers(Props.ActorTypeMembers)] TActor>(string name = null) |
| /// <param name="name">The name of the actor to create. The default value is <see langword="null"/>.</param> | ||
| /// <returns>A reference to the newly created actor</returns> | ||
| public abstract IActorRef SystemActorOf<TActor>(string name = null) where TActor : ActorBase, new(); | ||
| public abstract IActorRef SystemActorOf<[DynamicallyAccessedMembers(Props.ActorTypeMembers)] TActor>(string name = null) where TActor : ActorBase, new(); |
| /// <returns>The extension retrieved from the given actor system.</returns> | ||
| public static T WithExtension<T>(this ActorSystem system, Type extensionId) where T : class, IExtension | ||
| public static T WithExtension<T>(this ActorSystem system, | ||
| [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type extensionId) |
| /// What the trimmer has to keep on an actor type reached through <see cref="Props"/>: the public | ||
| /// constructors <see cref="Activator"/> needs, and the interface list the mailbox requirement check walks. | ||
| /// </summary> | ||
| internal const DynamicallyAccessedMemberTypes ActorTypeMembers = |
| /// the actor type; an implementation returning a <see cref="Type" /> from elsewhere has to carry the | ||
| /// same annotation on whatever it reads that type from. | ||
| /// </remarks> | ||
| [DynamicallyAccessedMembers(Props.ActorTypeMembers)] |
Aaronontheweb
removed this pull request from stack #8607
September 24, 2026 16:42
Aaronontheweb
added this pull request to stack #8632
September 24, 2026 16:43
Aaronontheweb
added a commit
to Aaronontheweb/akka.net
that referenced
this pull request
Sep 28, 2026
…hat couldn't fail, add switch-on version-skew coverage - Drop the LoadFirstPartyLoggerType try/catch helper: Type.GetType on a literal is a trimmer intrinsic, so the switch arm calls it directly (matches GetBuiltInLoggerType's style). throwOnError defaults to false, so a missing Akka.Hosting still returns null; a broken dll now surfaces its own exception instead of being swallowed. - BuiltInLoggersSpec's "never probes" test only named built-in loggers, so GetFirstPartyLoggerType never ran. It now uses CustomLoggerConfig, which is not built in, so the lookup actually executes and asserts Akka.Hosting is never probed. - Add a switch-on version-skew test to FirstPartyLoggerSpec: a Hosting logger name pinned to a different Akka.Hosting version now resolves with the DynamicTypeLoading switch on, not just off. Make the spec sealed and take ITestOutputHelper. - Amend the akkadotnet#8603 ledger row: switch-off apps can also use Akka.Hosting's LoggerFactoryLogger, not just the three core loggers. Add a new row for this PR.
Aaronontheweb
added a commit
that referenced
this pull request
Sep 29, 2026
…esolves without reflection (#8654) * AOT: resolve Akka.Hosting's LoggerFactoryLogger in akka.loggers without reflection With Akka.DynamicTypeLoading off, LoggingBus only accepted the three loggers in Akka.dll, so any Akka.Hosting app using AddLoggerFactory() failed at startup with a NotBuiltIn ConfigurationException. LoggingBus now checks a first-party table after the built-in one. A row matches only on the exact type name plus assembly (identity stripped, per #8613), then loads its own literal through a DynamicallyAccessedMembers string parameter so the trimmer keeps the type. An assembly that will not load counts as absent and falls through to today's behavior. Configs that name only core loggers never probe. Tests: switch-off startup and switch-on parity in Akka.Hosting.Tests; no-probe and absent-assembly checks in Akka.Tests. * Review fixes: simplify GetFirstPartyLoggerType, fix a no-probe test that couldn't fail, add switch-on version-skew coverage - Drop the LoadFirstPartyLoggerType try/catch helper: Type.GetType on a literal is a trimmer intrinsic, so the switch arm calls it directly (matches GetBuiltInLoggerType's style). throwOnError defaults to false, so a missing Akka.Hosting still returns null; a broken dll now surfaces its own exception instead of being swallowed. - BuiltInLoggersSpec's "never probes" test only named built-in loggers, so GetFirstPartyLoggerType never ran. It now uses CustomLoggerConfig, which is not built in, so the lookup actually executes and asserts Akka.Hosting is never probed. - Add a switch-on version-skew test to FirstPartyLoggerSpec: a Hosting logger name pinned to a different Akka.Hosting version now resolves with the DynamicTypeLoading switch on, not just off. Make the spec sealed and take ITestOutputHelper. - Amend the #8603 ledger row: switch-off apps can also use Akka.Hosting's LoggerFactoryLogger, not just the three core loggers. Add a new row for this PR.
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
Milestone-1 PR C of the Native AOT stack. Base:
aot/m1-b-mailboxes(PR B). Three commits.Why
Every actor Akka.NET creates is built through
Props, and two pieces of metadata have to survivetrimming for that to work: the public constructor
Activator.CreateInstancecalls, and theIRequiresMessageQueue<>interfacesMailboxes.HasRequiredTypereads off the actor type to pick amailbox. Neither was annotated, so the trimmer had no reason to keep either. The unrooted Native AOT
canary failed with
MissingMethodExceptiononAkka.Event.EventStreamUnsubscriberand onAkka.Actor.CoordinatedShutdownExtension, and withNo IProducesMessageQueue<TQueue> supplied for Akka.Event.LoggerMailboxType.Separately,
akka.loggerswas the last type-name setting on the local boot path still going straightthrough
Type.GetType, so the trimmer could not tell thatAkka.Event.DefaultLogger— the loggercore's own
akka.confnames — was loaded, and dropped it.What changed
1.
Keep actor constructor and interface metadata through the Props type flowOne named contract,
Props.ActorTypeMembers(PublicConstructors | Interfaces), on everyTypeandgeneric parameter that reaches
Activator.CreateInstanceor the mailbox requirement check: the sixType-takingPropsconstructors,Props.Type,Props.Create(Type, …), the threeCreate<TActor>overloads,
CreateBy<TProducer>,CreateProducer,ActivatorProducer,DefaultProducer,FactoryConsumer<>,DynamicProps<>,IIndirectActorProducer.ActorType,ActorRefFactoryExtensions.ActorOf<TActor>, andExtendedActorSystem.SystemActorOf<TActor>with itsActorSystemImploverride.ActorSystemWithExtensions.WithExtension<T>(ActorSystem, Type)andWithExtension<T, TI>take thenarrower
PublicParameterlessConstructor: those hand anIExtensionId— not an actor type — to theparameterless
Activator.CreateInstance, and that is exactly what the base branch's warnings askedfor.
2.
Resolve akka.loggers from a built-in tableStartDefaultLoggersmatches the configured name against the three loggers Akka.dll ships —DefaultLogger,StandardOutLoggerandTraceLogger, whose own XML doc tells users to name it here— and returns the type from a constant
typeof. Two spellings each — the bare nameakka.confshipsand the
"Ns.T, Akka"form — with the configured value run throughTypeExtensions.StripAssemblyIdentityfirst, so a fullAssemblyQualifiedNamematches the second keywhatever version, culture or public key token it names. A literal
typeof(T).AssemblyQualifiedNamekey would only ever match the one build that compiled it: measuredhere, that is
Version=1.5.60.0, so Akka.Hosting HOCON written against any other Akka version wouldmiss the table and, with the switch off, throw. A miss falls back to
Type.GetTypein a[RequiresUnreferencedCode]method that only runs while dynamic type loading is on; with the switchoff the site throws
AkkaFeatures.NotBuiltIn.The lookup is a
string[]matched inside one method with an annotated return, not aDictionary<string, Type>. A dictionary cannot carryDynamicallyAccessedMemberson its values, sothe type coming out of
TryGetValuearrives unannotated and the trimmer drops the constructorPropsneeds. That is measured, not theoretical: the first draft of this commit used a dictionary, the
publish reported
IL2067at theAddLoggercall, and the canary startedDefaultLoggeronly to diewith
MissingMethodException: No parameterless constructor defined for type 'Akka.Event.DefaultLogger'.3.
Carry the actor-type annotation through Akka.DependencyInjectionIIndirectActorProducer.ActorTypeis annotated in core now, so the threeIDependencyResolver.Propsmembers, their implementations on
DependencyResolver,ServiceProviderandServiceProviderDependencyResolver, andServiceProviderActorProducer(both theType-takingconstructor and the generic subclass) take
Props.ActorTypeMembers, which the project can namebecause core already grants it
InternalsVisibleTo.ActivatorUtilities.CreateInstanceasks forPublicConstructors, which that constant covers.Trim-warning count
Measured with the canary's README command on this base, deduped on code+file+line, counting only
warnings whose location is in
src/core/Akka/:aot/m1-b-mailboxes)-p:RootAkka=true(whole-library)Nine warnings removed, three added, plus one that only moved.
Removed (8 from commit 1, 1 from commit 2):
Added — all three only appear under
-p:RootAkka=true, because the unrooted canary never reachesthem, and none is on the local boot path:
Moved, not added:
IL2057 Actor/Props.cs:230→:242, theProps.TypeNamesetter'sType.GetType,shifted by the twelve lines this PR inserts above it.
Viral consequence for downstream generics
Nothing warns today, because no project in this repository sets
EnableTrimAnalyzer,IsTrimmableor
IsAotCompatible. When one does,Akka.TestKit's generic forwarders will reportIL2091untilthey carry the same constant:
TestKitBase.ActorOf<TActor>,ActorOfAsTestActorRef<TActor>,ActorOfAsTestFSMRef<TFsmActor, TState, TData>andTestProbe.ChildActorOf<T>all pass their type parameter toProps.Create<TActor>. The same appliesto any external
IIndirectActorProducer,IDependencyResolverimplementation orSystemActorOf<TActor>override — aDynamicallyAccessedMembersattribute is part of the contract,so an implementor has to repeat it verbatim rather than inherit it, and gets
IL2092/IL2093(parameter/return mismatch) or
IL2095(type-parameter mismatch) until it does.Canary status
The unrooted canary now boots fully with dynamic type loading switched off: both
ActorSystemscome up,
DefaultLoggerreally starts, bothAskround trips reply, and both terminate cleanly.It still exits 1, at the startup watchdog, on the four serializer warnings PR D removes:
With the watchdog and the two serializer assertions locally disabled (not committed), the canary
reaches
[canary] OKwith exit 0 and printsserializers, scheduler, log formatter and default mailbox all resolved, so A's scheduler and log-formatter tables and B's mailbox tables all passtheir positive assertions too. Serialization is the only thing left between this stack and a green
canary.
Public API
Attributes only — pre-approved by the repo owner.
CoreAPISpec.ApproveCore.DotNet.verified.txtgains[DynamicallyAccessedMembers(...)]on 16 members and nothing else: no signature, member or otherattribute changes.
Akka.DependencyInjectionhas no approval file, so commit 3 produces no churn.Tests
New
src/core/Akka.Tests/Event/BuiltInLoggersSpec.cs, in the non-parallelAkka.DynamicTypeLoadingcollection:AssemblyQualifiedName, and aVersion=99.0.0.0, PublicKeyToken=…one that can only resolvebecause the lookup strips assembly identity — with the switch off —
DefaultLoggerandTraceLoggerresolve the started logger actor by wildcardActorSelection(the logger name carries a process-global counter, so it cannot be hard-coded);
StandardOutLoggerasserts the system comes up, since a
MinimalLoggeris never started as an actor and onlysuppresses the stdout logger's removal — which is what
LoggerSpecrelies on.InitializeLogger.This is the switch-on regression guard.
ConfigurationExceptionnames the setting, thevalue and the switch.
Breaking changes
One row in
BREAKING_CHANGES_V1.6.md. No runtime behavior change from the attributes; with theswitch on,
akka.loggersresolves exactly as before for every input. With the switch off, only thebuilt-in loggers resolve.
Stack: PR 3 of 7 for AOT milestone 1. Base is PR #8602 (B); this PR shows only its own three commits. With A+B+C the canary boots, round-trips and terminates under Native AOT; it fails only on the four serializer startup warnings that PR D removes. Design and measurements: epic #7246.
Checklist