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
5 changes: 3 additions & 2 deletions BREAKING_CHANGES_V1.6.md

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions src/core/Akka.Tests/Dispatch/MailboxFeatureSwitchSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,33 @@ await AkkaFeaturesSpec.WithDynamicTypeLoading(false, async () =>
});
}

/// <summary>
/// Pins the widened matching rule the single-key table relies on: no space after the comma and a
/// lower-cased assembly name are both spellings <see cref="Type.GetType(string)"/> itself accepted,
/// and now <c>TypeExtensions.ToBuiltInAkkaTypeName</c> accepts them too.
/// </summary>
[Fact(DisplayName = "Mailboxes should resolve a built-in mailbox-type spelled without a comma space and with a lower-cased assembly name when dynamic type loading is off")]
public async Task Should_resolve_a_loosely_spelled_mailbox_type_When_dynamic_type_loading_is_disabled()
{
var config = ConfigurationFactory.ParseString(@"
loose-mailbox {
mailbox-type = ""Akka.Dispatch.BoundedDequeBasedMailbox,akka""
}");

await AkkaFeaturesSpec.WithDynamicTypeLoading(false, async () =>
{
var system = ActorSystem.Create("loose-mailbox-off", config);
try
{
system.Mailboxes.Lookup("loose-mailbox").Should().BeOfType<BoundedDequeBasedMailbox>();
}
finally
{
await system.Terminate();
}
});
}

/// <summary>
/// The explicit switch-ON regression guard for <c>mailbox-type</c>: the reflection fallback must still
/// resolve a <see cref="MailboxType"/> that <c>BuiltInMailboxTypes</c> knows nothing about.
Expand Down
2 changes: 2 additions & 0 deletions src/core/Akka.Tests/IO/DnsProviderConfigSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ await AkkaFeaturesSpec.WithDynamicTypeLoading(false, async () =>
[Theory(DisplayName = "DnsExt should resolve every spelling of the built-in provider name when dynamic type loading is off")]
[InlineData("Akka.IO.InetAddressDnsProvider")]
[InlineData("Akka.IO.InetAddressDnsProvider, Akka")]
[InlineData("Akka.IO.InetAddressDnsProvider,Akka")]
[InlineData("Akka.IO.InetAddressDnsProvider, AKKA")]
[InlineData("Akka.IO.InetAddressDnsProvider, Akka, Version=99.0.0.0, Culture=neutral, PublicKeyToken=null")]
public async Task Should_resolve_every_spelling_of_the_built_in_provider_When_dynamic_type_loading_is_disabled(string providerObjectName)
{
Expand Down
17 changes: 17 additions & 0 deletions src/core/Akka.Tests/Serialization/BuiltInSerializerDefaultsSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ private static Config ModuleStyleConfig(string boundTypeName) => ConfigurationFa
{
"Akka.Tests.Serialization.BuiltInSerializerDefaultsSpec+SomePoco",
"Akka.Tests.Serialization.BuiltInSerializerDefaultsSpec+SomePoco, Akka.Tests",
"Akka.Tests.Serialization.BuiltInSerializerDefaultsSpec+SomePoco ,akka.tests",
"Akka.Tests.Serialization.BuiltInSerializerDefaultsSpec+SomePoco, Akka.Tests, Version=99.0.0.0, Culture=neutral, PublicKeyToken=null"
};

Expand Down Expand Up @@ -292,6 +293,22 @@ await AkkaFeaturesSpec.WithDynamicTypeLoading(false,
}));
}

[Fact(DisplayName = "Serialization should reject a binding row that names a SerializationSetup type under the wrong assembly when dynamic type loading is off")]
public async Task Should_throw_ConfigurationException_When_a_binding_row_names_the_wrong_assembly_and_dynamic_type_loading_is_disabled()
{
const string boundTypeName = "Akka.Tests.Serialization.BuiltInSerializerDefaultsSpec+SomePoco, Some.Other.Assembly";
var setup = PocoSerializerSetup("poco", ModuleStyleConfig(boundTypeName), typeof(SomePoco));

await AkkaFeaturesSpec.WithDynamicTypeLoading(false, () =>
{
var exception = Assert.Throws<ConfigurationException>(
() => ActorSystem.Create("setup-wrong-assembly-off", setup));

exception.Message.Should().Contain(boundTypeName);
return Task.CompletedTask;
});
}

/// <remarks>
/// The serializer-row skip keys on the alias: a Setup that binds the type under an alias of its own
/// does not excuse a HOCON serializer row core cannot build.
Expand Down
44 changes: 44 additions & 0 deletions src/core/Akka.Tests/Util/AkkaFeaturesSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,50 @@ public void Should_strip_assembly_identity_From_a_qualified_type_name(string typ
Akka.Util.TypeExtensions.StripAssemblyIdentity(typeName).Should().Be(expected);
}

[Theory(DisplayName = "TypeExtensions.TrySplitTypeName should split a type name at the top-level comma")]
[InlineData("Akka.Event.SemanticLogMessageFormatter", "Akka.Event.SemanticLogMessageFormatter", null)]
[InlineData("Akka.Event.SemanticLogMessageFormatter, Akka", "Akka.Event.SemanticLogMessageFormatter", "Akka")]
[InlineData("Akka.Event.SemanticLogMessageFormatter,Akka", "Akka.Event.SemanticLogMessageFormatter", "Akka")]
[InlineData("Akka.Event.SemanticLogMessageFormatter, akka", "Akka.Event.SemanticLogMessageFormatter", "akka")]
[InlineData(
"Akka.Event.SemanticLogMessageFormatter, Akka, Version=1.5.60.0, Culture=neutral, PublicKeyToken=null",
"Akka.Event.SemanticLogMessageFormatter", "Akka")]
[InlineData(
"System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Int32, mscorlib]], mscorlib",
"System.Collections.Generic.Dictionary`2[[System.String, mscorlib],[System.Int32, mscorlib]]", "mscorlib")]
public void Should_split_a_type_name_at_the_top_level_comma(string typeName, string expectedName, string? expectedAssembly)
{
Akka.Util.TypeExtensions.TrySplitTypeName(typeName, out var name, out var assembly).Should().BeTrue();
name.Should().Be(expectedName);
assembly.Should().Be(expectedAssembly);
}

[Theory(DisplayName = "TypeExtensions.TrySplitTypeName should reject a null, empty or whitespace-only type name")]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Should_reject_an_empty_type_name(string? typeName)
{
Akka.Util.TypeExtensions.TrySplitTypeName(typeName, out _, out _).Should().BeFalse();
}

[Theory(DisplayName = "TypeExtensions.ToBuiltInAkkaTypeName should accept the bare name and every spelling of the Akka assembly")]
[InlineData("Akka.Event.SemanticLogMessageFormatter", "Akka.Event.SemanticLogMessageFormatter")]
[InlineData("Akka.Event.SemanticLogMessageFormatter, Akka", "Akka.Event.SemanticLogMessageFormatter")]
[InlineData("Akka.Event.SemanticLogMessageFormatter,Akka", "Akka.Event.SemanticLogMessageFormatter")]
[InlineData("Akka.Event.SemanticLogMessageFormatter, akka", "Akka.Event.SemanticLogMessageFormatter")]
[InlineData(
"Akka.Event.SemanticLogMessageFormatter, Akka, Version=99.0.0.0, Culture=neutral, PublicKeyToken=null",
"Akka.Event.SemanticLogMessageFormatter")]
[InlineData("Akka.Event.SemanticLogMessageFormatter, Contoso", null)]
[InlineData(null, null)]
[InlineData("", null)]
[InlineData(" ", null)]
public void Should_normalize_or_reject_a_type_name_When_computing_the_built_in_Akka_type_name(string? typeName, string? expected)
{
Akka.Util.TypeExtensions.ToBuiltInAkkaTypeName(typeName).Should().Be(expected);
}

/// <summary>
/// The explicit switch-ON regression guard: the reflection fallback must still resolve a type that no
/// <c>BuiltIn*</c> table knows about. Uses the log formatter rather than the scheduler because the
Expand Down
30 changes: 30 additions & 0 deletions src/core/Akka.Tests/Util/DynamicTypeLoadingConfigSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,36 @@ await AkkaFeaturesSpec.WithDynamicTypeLoading(false, async () =>
});
}

/// <summary>
/// Pins the widened matching rule the single-key table relies on: no space after the comma is a
/// spelling <see cref="Type.GetType(string)"/> itself accepted, and the table now accepts it too.
/// </summary>
[Fact(DisplayName = "Deployer should resolve a router type-mapping spelled without a comma space when dynamic type loading is off")]
public async Task Should_resolve_a_loosely_spelled_router_type_mapping_When_dynamic_type_loading_is_disabled()
{
var config = ConfigurationFactory.ParseString(@"
akka.actor.router.type-mapping.loose-round-robin-pool = ""Akka.Routing.RoundRobinPool,Akka""
akka.actor.deployment {
/loose {
router = loose-round-robin-pool
}
}");

await AkkaFeaturesSpec.WithDynamicTypeLoading(false, async () =>
{
var system = ActorSystem.Create("loose-router-off", config);
try
{
var deploy = ((ActorSystemImpl)system).Provider.Deployer.Lookup(new[] { "loose" });
deploy.RouterConfig.Should().BeOfType<RoundRobinPool>();
}
finally
{
await system.Terminate();
}
});
}

[Fact(DisplayName = "Deployer should reject a router type-mapping that is not built in when dynamic type loading is off")]
public async Task Should_throw_ConfigurationException_When_the_router_is_not_built_in_and_dynamic_type_loading_is_disabled()
{
Expand Down
21 changes: 5 additions & 16 deletions src/core/Akka/Actor/Deployer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,7 @@ public class Deployer
/// Builds <see cref="BuiltInRouterConfigs"/>: the 13 routers <c>akka.actor.router.type-mapping</c>
/// maps onto types inside Akka.dll and that can actually reach this table.
///
/// Two spellings per router, both deliberate: <c>akka.conf</c> ships the bare name and HOCON in the
/// wild also carries the <c>Ns.T, Akka</c> form. The lookup runs the configured value through
/// <see cref="Akka.Util.TypeExtensions.StripAssemblyIdentity"/> first, so a full
/// <see cref="Type.AssemblyQualifiedName"/> - which Akka.Hosting writes into HOCON - matches the
/// second key whatever version, culture or public key token it names. 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.
/// Keyed by bare type name; TypeExtensions.ToBuiltInAkkaTypeName normalizes what HOCON carries.
/// </summary>
private static Dictionary<string, Func<Config, RouterConfig>> BuildBuiltInRouterConfigs()
{
Expand All @@ -72,15 +65,11 @@ private static Dictionary<string, Func<Config, RouterConfig>> BuildBuiltInRouter
return builtIn;

// typeof(TRouter) is what keeps this trimmer-safe: the trimmer sees the type, keeps it, and hands
// us its own names, so no spelling can drift out of step with the type it maps to.
// us its own name, so the key can't drift out of step with the type it maps to.
void Add<TRouter>(Func<Config, RouterConfig> factory) where TRouter : RouterConfig
{
var routerType = typeof(TRouter);

// "Akka.Routing.RoundRobinPool"
builtIn[routerType.FullName] = factory;
// "Akka.Routing.RoundRobinPool, Akka"
builtIn[$"{routerType.FullName}, {routerType.Assembly.GetName().Name}"] = factory;
builtIn[typeof(TRouter).FullName] = factory;
}
}

Expand Down Expand Up @@ -213,8 +202,8 @@ private RouterConfig CreateRouterConfig(string routerTypeAlias, Config deploymen
throw new ConfigurationException(message);
}

if (BuiltInRouterConfigs.TryGetValue(
Akka.Util.TypeExtensions.StripAssemblyIdentity(routerTypeName), out var routerFactory))
if (Akka.Util.TypeExtensions.ToBuiltInAkkaTypeName(routerTypeName) is { } builtInRouterTypeName &&
BuiltInRouterConfigs.TryGetValue(builtInRouterTypeName, out var routerFactory))
return routerFactory(deployment);

if (!AkkaFeatures.IsDynamicTypeLoadingSupported)
Expand Down
15 changes: 4 additions & 11 deletions src/core/Akka/Actor/Internal/ActorSystemImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -284,26 +284,19 @@ public override ActorSelection ActorSelection(string actorPath)
// The akka.scheduler.implementation values that ship inside Akka.dll, constructed directly so that
// neither the trimmer nor the Native AOT compiler has to see through a Type.GetType call.
//
// Two spellings, both deliberate: akka.conf ships the bare name and HOCON in the wild also carries the
// "Ns.T, Akka" form. The lookup runs the configured value through
// TypeExtensions.StripAssemblyIdentity first, so a full AssemblyQualifiedName - which Akka.Hosting
// writes into HOCON - matches the second key whatever version, culture or public key token it names.
// 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.
// Keyed by bare type name; TypeExtensions.ToBuiltInAkkaTypeName normalizes what HOCON carries.
private static readonly Dictionary<string, Func<Config, ILoggingAdapter, IScheduler>> BuiltInSchedulers =
new(StringComparer.Ordinal)
{
["Akka.Actor.HashedWheelTimerScheduler"] = static (config, log) => new HashedWheelTimerScheduler(config, log),
["Akka.Actor.HashedWheelTimerScheduler, Akka"] = static (config, log) => new HashedWheelTimerScheduler(config, log)
["Akka.Actor.HashedWheelTimerScheduler"] = static (config, log) => new HashedWheelTimerScheduler(config, log)
};

private void ConfigureScheduler()
{
var schedulerClass = _settings.SchedulerClass;
// fully qualified: this file also imports System.Reflection, which has its own TypeExtensions
if (BuiltInSchedulers.TryGetValue(
Util.TypeExtensions.StripAssemblyIdentity(schedulerClass), out var factory))
if (Util.TypeExtensions.ToBuiltInAkkaTypeName(schedulerClass) is { } builtInSchedulerName &&
BuiltInSchedulers.TryGetValue(builtInSchedulerName, out var factory))
{
_scheduler = factory(_settings.Config, Log);
}
Expand Down
31 changes: 8 additions & 23 deletions src/core/Akka/Actor/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,37 +30,22 @@ public class Settings
// The akka.stdout-logger-class values that ship inside Akka.dll, constructed directly so the trimmer
// and the Native AOT compiler can see the type without looking through Type.GetType.
//
// akka.conf leaves this setting empty, so the table is only ever reached from user config. Two
// spellings, both deliberate: the bare name and the "Ns.T, Akka" form, which is what HOCON in the
// wild carries. The lookup runs the configured value through TypeExtensions.StripAssemblyIdentity
// first, so a full AssemblyQualifiedName - which Akka.Hosting writes into HOCON - matches the second
// key whatever version, culture or public key token it names. 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.
// Keyed by bare type name; TypeExtensions.ToBuiltInAkkaTypeName normalizes what HOCON carries.
private static readonly Dictionary<string, Func<MinimalLogger>> BuiltInStdoutLoggers =
new(StringComparer.Ordinal)
{
["Akka.Event.StandardOutLogger"] = static () => new StandardOutLogger(),
["Akka.Event.StandardOutLogger, Akka"] = static () => new StandardOutLogger()
["Akka.Event.StandardOutLogger"] = static () => new StandardOutLogger()
};

// The akka.logger-formatter values that ship inside Akka.dll, constructed directly so the trimmer and
// the Native AOT compiler can see the type without looking through Type.GetType.
//
// Two spellings, both deliberate: akka.conf ships the "Ns.T, Akka" form and HOCON in the wild also
// carries the bare name. The lookup runs the configured value through
// TypeExtensions.StripAssemblyIdentity first, so a full AssemblyQualifiedName - which Akka.Hosting
// writes into HOCON - matches the second key whatever version, culture or public key token it names.
// 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.
// Keyed by bare type name; TypeExtensions.ToBuiltInAkkaTypeName normalizes what HOCON carries.
private static readonly Dictionary<string, Func<ILogMessageFormatter>> BuiltInLogMessageFormatters =
new(StringComparer.Ordinal)
{
["Akka.Event.DefaultLogMessageFormatter"] = static () => DefaultLogMessageFormatter.Instance,
["Akka.Event.DefaultLogMessageFormatter, Akka"] = static () => DefaultLogMessageFormatter.Instance,
["Akka.Event.SemanticLogMessageFormatter"] = static () => SemanticLogMessageFormatter.Instance,
["Akka.Event.SemanticLogMessageFormatter, Akka"] = static () => SemanticLogMessageFormatter.Instance
["Akka.Event.SemanticLogMessageFormatter"] = static () => SemanticLogMessageFormatter.Instance
};

private readonly Config _userConfig;
Expand Down Expand Up @@ -180,8 +165,8 @@ public Settings(ActorSystem system, Config config, ActorSystemSetup setup)
{
StdoutLogger = new StandardOutLogger();
}
else if (BuiltInStdoutLoggers.TryGetValue(
TypeExtensions.StripAssemblyIdentity(stdoutClassName), out var stdoutLoggerFactory))
else if (TypeExtensions.ToBuiltInAkkaTypeName(stdoutClassName) is { } builtInStdoutLoggerName &&
BuiltInStdoutLoggers.TryGetValue(builtInStdoutLoggerName, out var stdoutLoggerFactory))
{
StdoutLogger = stdoutLoggerFactory();
}
Expand All @@ -208,8 +193,8 @@ public Settings(ActorSystem system, Config config, ActorSystemSetup setup)
{
LogFormatter = DefaultLogMessageFormatter.Instance;
}
else if (BuiltInLogMessageFormatters.TryGetValue(
TypeExtensions.StripAssemblyIdentity(loggerFormatterName), out var logFormatterFactory))
else if (TypeExtensions.ToBuiltInAkkaTypeName(loggerFormatterName) is { } builtInLogFormatterName &&
BuiltInLogMessageFormatters.TryGetValue(builtInLogFormatterName, out var logFormatterFactory))
{
LogFormatter = logFormatterFactory();
}
Expand Down
Loading
Loading