Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions BREAKING_CHANGES_V1.6.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ of `Behavior`, `Wire`, `API` (combine with `+`).

| Status | PR / Branch | Component | Type | Change | Migration |
|--------|-------------|-----------|------|--------|-----------|
| Planned | `aot/m1-d-serializer-defaults` | `Akka.Serialization` | Behavior | `SerializerIdentifierHelper.GetSerializerIdentifierFromConfig` matches `akka.actor.serialization-identifiers` keys against the name of the `Type` in hand instead of calling `Type.GetType(key, throwOnError: true)` on every key. Fixes one unloadable key in the block taking `ActorSystem.Create` down with it; narrows matching to string comparison, and widens it by ignoring the assembly identity and by letting a bare `FullName` key match a type in any assembly. | Spell keys `"Namespace.Serializer, AssemblyName"`, optionally with the `Version`/`Culture`/`PublicKeyToken` tail. Prefer assembly-qualified keys: two serializers with the same `FullName` in different assemblies would now share whichever bare key matches first, which corrupts the wire. |
| Planned | `aot/m1-d-serializer-defaults` | `Akka.Serialization` | Behavior | `Serialization..ctor` registers the `bytes`/`json` serializers and the `System.Byte[]`/`System.Object` bindings from built-in tables instead of `Type.GetType`, falling back to reflection while `Akka.DynamicTypeLoading` is on and throwing a `ConfigurationException` when it is off. With the switch off core does not register `json` at all, so a type with no binding of its own has no fallback serializer. A built-in serializer is also constructed directly now. | None at the default. An application that turns `Akka.DynamicTypeLoading` off must register a serializer for `System.Object` (or per message type) through a `SerializationSetup`; pointing the existing `"System.Object"` binding at that alias works. A `SerializationSetup` also covers the HOCON rows it takes over, such as a module's `reference.conf` rows: a serializer row for an alias the Setup registers is skipped (its `serialization-settings` block goes unused), and a binding row resolves if it names one of the Setup's `UseFor` types by full name. Any other row core can't resolve still throws. Code catching `TargetInvocationException` around a serializer constructor should catch the inner exception type. |
| Planned | `aot/m1-c-props-and-loggers` | `Akka` (core / `Akka.Actor.Props`, `Akka.Event.LoggingBus`), `Akka.DependencyInjection` | Behavior + API | Two changes. (1) The whole `Props` actor-type flow now carries one trimming contract, `[DynamicallyAccessedMembers(PublicConstructors \| Interfaces)]`, so the trimmer keeps the constructor `Activator` calls and the `IRequiresMessageQueue<>` interfaces the mailbox requirement check reads off the actor type: the `Type`-taking `Props` constructors, `Props.Type`, `Props.Create(Type, params object[])`, all three `Props.Create<TActor>` overloads, `Props.CreateBy<TProducer>`, `IIndirectActorProducer.ActorType`, `ActorRefFactoryExtensions.ActorOf<TActor>`, `ExtendedActorSystem.SystemActorOf<TActor>` (and the `ActorSystemImpl` override), plus -- in `Akka.DependencyInjection` -- the three `IDependencyResolver.Props` members and their implementations on `DependencyResolver`, `ServiceProvider` and `ServiceProviderDependencyResolver`, and the internal `ServiceProviderActorProducer`. Separately, `ActorSystemWithExtensions.WithExtension<T>(ActorSystem, Type)` and `WithExtension<T, TI>` gain `[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]` -- the narrower contract, because those take an `IExtensionId`, not an actor type, and call the parameterless `Activator.CreateInstance`. No runtime behavior changes; the attributes are visible only to trim analysis and change no signature a compiler binds to. (2) `LoggingBus.StartDefaultLoggers` resolves `akka.loggers` from new built-in name tables (all three loggers Akka.dll ships -- `Akka.Event.DefaultLogger`, `Akka.Event.StandardOutLogger` and `Akka.Event.TraceLogger` -- each under three spellings: the bare name core's own `akka.conf` ships, the short assembly-qualified name that `TraceLogger`'s own documentation tells users to write, and the full versioned assembly-qualified name Akka.Hosting writes) before falling back to `Type.GetType`, and that fallback only runs while dynamic type loading is on. With the switch on, which is what every existing application gets, the same config resolves to the same logger as before, for every input. With the switch off, only the names in the table resolve: any other logger throws a `ConfigurationException` naming `akka.loggers`, the value and the switch, where it previously threw `ConfigurationException` "Logger specified in config cannot be found". | None at the default -- the switch is on unless a project file explicitly turns it off. A trimmed or Native AOT application that turns the switch off must leave `akka.loggers` at the built-in loggers. The new attributes need no source change, and nothing warns unless a project sets `EnableTrimAnalyzer` / `IsTrimmable` / `IsAotCompatible` -- no project in this repository does yet. Once one does: passing an unannotated `Type` into any of these members reports `IL2072` when it came from a method return or a field, and `IL2067` when it came from one of the caller's own parameters; an unannotated generic argument reports `IL2091`. Fix either by annotating the `Type` or type parameter it flows from with `[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors \| DynamicallyAccessedMemberTypes.Interfaces)]`, or by passing a `typeof(...)`. Because the attribute is part of the contract, an *implementor* has to repeat it verbatim rather than flow it: a class implementing `IIndirectActorProducer.ActorType` or any of the three `IDependencyResolver.Props` members reports `IL2092` / `IL2093` (parameter and return annotation mismatch) or `IL2095` (type-parameter mismatch) until it carries the identical attribute, and so does an override of `ExtendedActorSystem.SystemActorOf<TActor>`. |
| Planned | `aot/m1-b-mailboxes` | `Akka` (core / `Akka.Dispatch.Mailboxes`) | Behavior + API | `Mailboxes` resolves the mailbox names core's own `akka.conf` ships from two built-in tables (`BuiltInMailboxTypes`, `BuiltInMessageQueueSemantics`) instead of `Type.GetType`, so the trimmer and the Native AOT compiler can see which type is loaded. Two observable changes. (1) A built-in `mailbox-type` is constructed directly, so when its constructor rejects its config the `ArgumentException` from `Mailboxes.Lookup` carries that constructor's own exception as `InnerException` instead of the `TargetInvocationException` `Activator.CreateInstance` wrapped it in, and names the configured spelling (e.g. `Akka.Dispatch.BoundedMailbox, Akka`) where it previously named `Type.ToString()`; relatedly, the non-zero-push-timeout warning now runs outside that `try`, so an exception from a custom `IProducesPushTimeoutSemanticsMailbox.PushTimeout` getter propagates as itself rather than wrapped. (2) With the `Akka.DynamicTypeLoading` switch **off**, a key under `akka.actor.mailbox.requirements` that is neither built in nor resolvable now throws a `ConfigurationException` out of `ActorSystem.Create`, where the switch-on path still logs a warning and skips that one binding; an unknown `mailbox-type` or `mailbox-requirement` likewise throws a `ConfigurationException` naming the setting, the value and the switch, in place of the previous `ConfigurationException` / `TypeLoadException`. With the switch on - the default, and what every existing application gets - every other input resolves to the same mailbox as before. Separately, `MailboxType` and the `Type` parameters of `Mailboxes.HasRequiredType`, `Mailboxes.ProducesMessageQueue` and `Mailboxes.GetRequiredType` gain `[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)]` so that trimming keeps `IProducesMessageQueue<>` / `IRequiresMessageQueue<>` on subclasses. | None at the default -- the switch is on unless a project file explicitly turns it off. Code that caught `TargetInvocationException` around a rejected `mailbox-capacity` / `mailbox-push-timeout-time` should catch the inner exception type instead, and code asserting on the exact `Cannot instantiate MailboxType ...` text should expect the configured spelling for the five built-in types. A trimmed or Native AOT application that turns the switch off must keep `mailbox-type`, `mailbox-requirement` and every key under `akka.actor.mailbox.requirements` at built-in values. The new attributes are visible only to trim analysis -- they change no signature a compiler binds to -- but a caller that has trim analysis enabled and passes an unannotated `Type` into those three methods now gets `IL2072` at its own call site: annotate the `Type` it flows from with `[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)]`, or pass a `typeof(...)`. |
| Planned | `aot/m1-a-feature-switch-and-first-tables` | `Akka` (core / actor system bootstrap) | Behavior | Adds the internal trimming / Native AOT feature switch `Akka.DynamicTypeLoading` (default **on**) and puts built-in lookup tables in front of the first three HOCON type-name sites: `akka.stdout-logger-class` and `akka.logger-formatter` in `Settings`, and `akka.scheduler.implementation` in `ActorSystemImpl.ConfigureScheduler`. Each site now checks a `BuiltIn*` table before falling back to `Type.GetType`. Each table carries two spellings of every name it knows -- the bare `Ns.T` and the `Ns.T, Akka` form, which is what core's own `akka.conf` uses -- and the lookup first runs the configured value through the new internal `Akka.Util.TypeExtensions.StripAssemblyIdentity`, which removes the `Version=`, `Culture=`, `PublicKeyToken=`, `ProcessorArchitecture=`, `Retargetable=` and `ContentType=` components. With the switch on, which is what every existing application gets, the same config resolves to the same type as before, with two narrow differences: (1) a built-in name is constructed directly, so a constructor that rejects its config (e.g. `akka.scheduler.tick-duration` below 10 ms, which `HashedWheelTimerScheduler` refuses) now surfaces its own `ArgumentOutOfRangeException` / `ConfigurationException` instead of the `TargetInvocationException` that `Activator.CreateInstance` wrapped it in; (2) a built-in type name written with a full assembly-qualified name of a different Akka version (what `Akka.Hosting` writes into HOCON, carried across an upgrade) now resolves where it previously threw. `Type.GetType` rejects the version mismatch, so on `dev` such a value fails in both switch states -- measured: `ArgumentException("Could not load type of ...")` for `akka.logger-formatter`/`akka.stdout-logger-class`, and `FileNotFoundException` for `akka.scheduler.implementation`, whose `Type.GetType` call passes `throwOnError: true`. Custom (non-built-in) names are unchanged, since the reflection fallback still receives the raw value. An application that turns the switch off with `<RuntimeHostConfigurationOption Include="Akka.DynamicTypeLoading" Value="false" Trim="true" />` gets only the names in the tables: anything else throws a `ConfigurationException` naming the setting, the value and the switch, where it previously threw `TypeLoadException` (scheduler) or `ArgumentException` (stdout logger, log formatter). | None at the default -- the switch is on unless a project file explicitly turns it off. Code that caught `TargetInvocationException` around `ActorSystem.Create` to read a rejected `akka.scheduler.*` value should catch the inner exception type instead. A trimmed or Native AOT application that turns the switch off must leave these three settings at their built-in values. |
Expand Down
34 changes: 31 additions & 3 deletions src/aot/Akka.AOT.App/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,19 @@ private static async Task RunAsync(string label, Func<LogFilterSetup, ActorSyste
/// </summary>
private static void AssertBuiltInsResolved(string label, ActorSystem system)
{
Require(label, system.Serialization.FindSerializerFor("hello") is not null,
"no serializer for a string - akka.actor.serializers/serialization-bindings did not register");
Require(label, system.Serialization.FindSerializerFor(new byte[] { 1 }) is not null,
"no serializer for a byte[] - akka.actor.serializers/serialization-bindings did not register");

// ByteArraySerializer needs no reflection, so 'bytes' and the System.Byte[] binding survive with the
// switch off. Newtonsoft.Json does not, so core registers neither the 'json' alias nor the
// System.Object binding that points at it, and a type with no binding of its own has no fallback. That
// is the designed behavior, not a gap - so assert the throw, and assert the message tells the user what
// to do about it.
var unbound = RequireThrows(label, () => system.Serialization.FindSerializerFor("hello"));

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 comment above is correct about both the design and the assertion.

Require(label, unbound.Message.Contains("Akka.DynamicTypeLoading", StringComparison.Ordinal)
&& unbound.Message.Contains("SerializationSetup", StringComparison.Ordinal),
$"serializing an unbound type threw, but without saying why: [{unbound.Message}]");

Require(label, system.Scheduler is HashedWheelTimerScheduler,
$"akka.scheduler.implementation resolved to [{system.Scheduler.GetType().FullName}]");
Require(label, system.Settings.LogFormatter is SemanticLogMessageFormatter,
Expand All @@ -102,7 +111,7 @@ private static void AssertBuiltInsResolved(string label, ActorSystem system)
Require(label, defaultMailbox is UnboundedMailbox,
$"akka.actor.default-mailbox resolved to [{defaultMailbox.GetType().FullName}]");

Console.WriteLine($"[canary] {label}: serializers, scheduler, log formatter and default mailbox all resolved");
Console.WriteLine($"[canary] {label}: byte[] serializer, scheduler, log formatter and default mailbox all resolved, unbound types throw as designed");
}

private static void Require(string label, bool condition, string problem)
Expand All @@ -111,6 +120,25 @@ private static void Require(string label, bool condition, string problem)
throw new InvalidOperationException($"{label}: {problem}");
}

/// <summary>
/// Asserts that <paramref name="action"/> fails with a <see cref="System.Runtime.Serialization.SerializationException"/> and hands the
/// exception back so the caller can assert on its message.
/// </summary>
private static System.Runtime.Serialization.SerializationException RequireThrows(string label, Action action)
{
try
{
action();
}
catch (System.Runtime.Serialization.SerializationException ex)
{
return ex;
}

throw new InvalidOperationException(
$"{label}: serializing a type with no serialization-binding was expected to throw with dynamic type loading off, but it succeeded");
}

private static void PrintFailure(Exception? ex)
{
if (ex is null)
Expand Down
12 changes: 8 additions & 4 deletions src/aot/Akka.AOT.App/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,14 @@ hurdles:
`The type name for serializer 'json' did not resolve to an actual Type`,
`Serialization binding to non existing serializer: 'bytes'` and
`Mailbox Requirement mapping [...] is not an actual type`.
2. **Positive assertions.** After boot: `Serialization.FindSerializerFor` returns a serializer for
both a `string` and a `byte[]`, `Scheduler` is a `HashedWheelTimerScheduler`,
`Settings.LogFormatter` is a `SemanticLogMessageFormatter`, and
`Mailboxes.Lookup("akka.actor.default-mailbox")` is an `UnboundedMailbox`.
2. **Positive assertions.** After boot: `Serialization.FindSerializerFor` returns a serializer for a
`byte[]`, `Scheduler` is a `HashedWheelTimerScheduler`, `Settings.LogFormatter` is a
`SemanticLogMessageFormatter`, and `Mailboxes.Lookup("akka.actor.default-mailbox")` is an
`UnboundedMailbox`. With the switch off a type that has no `serialization-bindings` entry of its
own throws by design - core does not register the reflection-driven `json` serializer, nor the
`System.Object` binding pointing at it - so the canary asserts that a `string` throws with a
message naming the switch and `SerializationSetup`; a real AOT application registers a serializer
for its own message types through a `SerializationSetup`.

Shutdown is bounded (`Terminate()` with a 30 s cap) and `AppDomain.UnhandledException` prints the
same failure block, so a crash on a pool thread cannot exit quietly.
Expand Down
Loading
Loading