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
1 change: 1 addition & 0 deletions BREAKING_CHANGES_V1.6.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ of `Behavior`, `Wire`, `API` (combine with `+`).

| Status | PR / Branch | Component | Type | Change | Migration |
|--------|-------------|-----------|------|--------|-----------|
| Planned | `fix/aot-provider-constant-type-names` | `Akka` (core / actor system bootstrap) | Behavior | `ActorSystemImpl` now resolves the three built-in actor ref providers from the compile-time constant type names on `ProviderSelection`, with the `Type.GetType` call inlined at each site, so the trimmer and the Native AOT compiler can see which type is loaded and keep it. Two observable consequences: (1) `ProviderSelection.GetProvider` now maps the local provider's type name -- both the bare `"Akka.Actor.LocalActorRefProvider"` that ships in `akka.conf` and the assembly-qualified `"Akka.Actor.LocalActorRefProvider, Akka"` -- onto `ProviderSelection.Local` instead of letting it fall through to `Custom`, so `Settings.ProviderClass` on a default boot now reports `"Akka.Actor.LocalActorRefProvider, Akka"` where it previously reported the bare `"Akka.Actor.LocalActorRefProvider"`. (2) `Settings` only validates `ProviderClass` through `Type.GetType` when the selection is `ProviderSelection.Custom`; the built-in three are validated where they are constructed, and a missing `Akka.Remote` / `Akka.Cluster` reference now throws `ConfigurationException("akka.actor.provider = cluster, but Akka.Cluster is not referenced by this application.")` from `ActorSystemImpl` rather than the generic "not a valid type name" message from `Settings`. Which provider gets constructed for any given config is unchanged. | None for normal use. Code that compares `Settings.ProviderClass` against the bare string `"Akka.Actor.LocalActorRefProvider"` should compare against `ProviderSelection.Local.Instance.Fqn`, or test `Settings.ProviderSelectionType is ProviderSelection.Local`. Code that asserted on the exact text of the misconfiguration exception for `provider = remote` / `provider = cluster` should expect the new message. |
| Planned | `feature/hosting-import-dev` (forward-port of [#8591](https://github.com/akkadotnet/akka.net/pull/8591)) | `Akka.Hosting`, `Akka.Remote.Hosting`, `Akka.Cluster.Hosting`, `Akka.Persistence.Hosting`, `Akka.Hosting.TestKit`, `Akka.Hosting.TestKit.Xunit2` | API | The Akka.Hosting packages now ship from this repository at the Akka.NET version. On 1.6 they target `net10.0` only (previously `netstandard2.0;net6.0`), matching the rest of Akka.NET 1.6. `Microsoft.Extensions.*` floor moves to 10.0.0 for `Akka`, `Akka.DependencyInjection` and the Hosting packages. | Reference `Akka.Hosting.*` at the same version as `Akka.*`. Applications must target `net10.0` or later, as for every other Akka.NET 1.6 package. |
| Planned | `fix/testkit-xunit-async-dispose-chain` | `Akka.TestKit.Xunit` | API + Behavior | `Akka.TestKit.Xunit.TestKit` now also implements `Xunit.IAsyncLifetime`, adding `virtual ValueTask InitializeAsync()` and `virtual ValueTask DisposeAsync()`. This closes #8191: xUnit v3 tears a test class instance down via `IAsyncDisposable.DisposeAsync()` in preference to `IDisposable.Dispose()` whenever a type implements both, so a derived spec that declared its own `IAsyncLifetime.DisposeAsync()` (to run its own async teardown) short-circuited `TestKit.Dispose(bool)` entirely and silently leaked its `ActorSystem` -- DotNetty threads, a bound socket, and a `PeriodicTimer`-driven scheduler included. `TestKit.DisposeAsync()` now runs the synchronous dispose chain (`AfterAll()` etc.) and then shuts the system down with the non-blocking `ShutdownAsync()` instead of the blocking `Shutdown()`. | A derived class that already declares its own `InitializeAsync`/`DisposeAsync` (whether via `IAsyncLifetime` directly or by re-declaring the method) now hides a virtual member of the same name and gets `CS0114`, which is an error under this repo's own `-warnaserror` build. Add `override` to each and chain to the base (`await base.InitializeAsync();` / `await base.DisposeAsync();`), and drop the redundant `IAsyncLifetime` interface declaration and any no-op `DisposeAsync` that only returned a completed `ValueTask`. |
| Planned | `fix/8508-clusterclient-reconnect-timer` | `Akka.Cluster.Tools` | API + Behavior | `ClusterClient` now implements `Akka.Actor.IWithTimers` and manages its reconnect, heartbeat and refresh-contacts timers through the keyed timer scheduler. The reconnect deadline is armed once per entry into the establishing phase (`PreStart` and re-establish) instead of on every message handled while establishing, which is the fix for #8508: the old code abandoned one one-shot timer per message and cancelled only the last, so a leftover firing after the client returned to establishing stopped a healthy client mid-reconnect, long before its own deadline was due. API surface: the class gains the `IWithTimers` interface and a public `Timers` property; the `PostStop` override is gone because the timer scheduler cancels timers on stop and restart. `reconnect-timeout = off` (the default) remains a true no-op and arms nothing. Secondary behavior change: because the timers are keyed, a `RefreshContactsTick` still queued from the previous cadence is discarded when the interval changes on a state transition, rather than being delivered against the new cadence. | None. `ClusterClient` is `sealed`, so the added interface and property cannot break a subclass, and the removed `PostStop` override was `protected` on that sealed class and therefore unreachable from outside. Deployments that had enabled `akka.cluster.client.reconnect-timeout` should simply stop seeing clients stopped early during receptionist handover. |
Expand Down
40 changes: 40 additions & 0 deletions src/core/Akka.Cluster.Tests/ProviderTypeNameSpec.cs

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.

Validates that the constant is correct - makes sense.

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//-----------------------------------------------------------------------
// <copyright file="ProviderTypeNameSpec.cs" company="Akka.NET Project">
// Copyright (C) 2009-2022 Lightbend Inc. <http://www.lightbend.com>
// Copyright (C) 2013-2025 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
//-----------------------------------------------------------------------

#nullable enable

using System;
using Akka.Actor;
using FluentAssertions;
using Xunit;

namespace Akka.Cluster.Tests
{
/// <summary>
/// The <see cref="ProviderSelection"/> type-name constants are the only thing standing between
/// <c>akka.actor.provider</c> and the provider type now that <c>ActorSystemImpl.CreateProvider</c> resolves
/// them through an annotated string parameter. Renaming or moving a provider would not break the build --
/// only the runtime lookup, and only in a trimmed or AOT-published app. This spec turns that into a test
/// failure. It lives in Akka.Cluster.Tests because that project references Akka.Remote as well, so both
/// constants resolve here. The local provider is constructed directly and never goes through its constant.
/// </summary>
public class ProviderTypeNameSpec
{
[Theory(DisplayName = "Should_resolve_an_IActorRefProvider_When_loading_a_ProviderSelection_type_name_constant")]
[InlineData(ProviderSelection.RemoteActorRefProvider)]
[InlineData(ProviderSelection.ClusterActorRefProvider)]
public void Should_resolve_an_IActorRefProvider_When_loading_a_ProviderSelection_type_name_constant(string typeName)
{
var providerType = Type.GetType(typeName);

providerType.Should().NotBeNull(
$"[{typeName}] must stay resolvable - ActorSystemImpl resolves built-in providers from these constants");
typeof(IActorRefProvider).IsAssignableFrom(providerType).Should().BeTrue(
$"[{typeName}] must name an {nameof(IActorRefProvider)} implementation");
}
}
}
43 changes: 43 additions & 0 deletions src/core/Akka.Tests/Actor/ProviderSelectionSpec.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//-----------------------------------------------------------------------
// <copyright file="ProviderSelectionSpec.cs" company="Akka.NET Project">
// Copyright (C) 2009-2022 Lightbend Inc. <http://www.lightbend.com>
// Copyright (C) 2013-2025 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
//-----------------------------------------------------------------------

#nullable enable

using Akka.Actor;
using Akka.Configuration;
using FluentAssertions;
using Xunit;

namespace Akka.Tests.Actor
{
/// <summary>
/// <c>ActorSystemImpl.ConfigureProvider</c> only takes the trimmer-friendly
/// <see cref="LocalActorRefProvider"/> branch when <see cref="ProviderSelection.GetProvider"/> maps the
/// configured provider onto <see cref="ProviderSelection.Local"/>, so every spelling of the local provider
/// type name has to land there - including the bare one that ships in <c>akka.conf</c>.
/// </summary>
public class ProviderSelectionSpec
{
[Theory(DisplayName = "Should_return_Local_When_GetProvider_is_given_a_local_provider_type_name")]
[InlineData("local")] // the alias
[InlineData("Akka.Actor.LocalActorRefProvider")] // the bare type name that ships in akka.conf
[InlineData(ProviderSelection.LocalActorRefProvider)] // "Akka.Actor.LocalActorRefProvider, Akka"
public void Should_return_Local_When_GetProvider_is_given_a_local_provider_type_name(string providerClass)
{
ProviderSelection.GetProvider(providerClass)
.Should().BeSameAs(ProviderSelection.Local.Instance);
}

[Fact(DisplayName = "Should_report_the_assembly_qualified_local_provider_name_When_the_default_config_is_used")]
public void Should_report_the_assembly_qualified_local_provider_name_When_the_default_config_is_used()
{
var settings = new Settings(null, ConfigurationFactory.Default());

settings.ProviderSelectionType.Should().BeSameAs(ProviderSelection.Local.Instance);
}
}
}
3 changes: 2 additions & 1 deletion src/core/Akka.Tests/Configuration/ConfigurationSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ public void The_default_configuration_file_contain_all_configuration_properties(
settings.LogDeadLettersSuspendDuration.ShouldBe(TimeSpan.FromMinutes(5));
settings.LogFormatter.Should().BeOfType<SemanticLogMessageFormatter>();

settings.ProviderClass.ShouldBe(typeof (LocalActorRefProvider).FullName);
// the default provider resolves to ProviderSelection.Local, so ProviderClass reports the assembly-qualified name
settings.ProviderClass.ShouldBe(ProviderSelection.Local.Instance.Fqn);
settings.SupervisorStrategyClass.ShouldBe(typeof (DefaultSupervisorStrategy).FullName);
settings.CreationTimeout.Seconds.ShouldBe(20);
settings.AskTimeout.ShouldBe(Timeout.InfiniteTimeSpan);
Expand Down
2 changes: 2 additions & 0 deletions src/core/Akka/Actor/ActorSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ internal static ProviderSelection GetProvider(string providerClass)
switch (providerClass)
{
case "local":
case "Akka.Actor.LocalActorRefProvider": // additional case for the bare type name used by akka.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.

Good, adds a measure of backwards compat

case LocalActorRefProvider: // additional case for older configurations
return Local.Instance;
case "remote":
case RemoteActorRefProvider: // additional case for older configurations
Expand Down
54 changes: 47 additions & 7 deletions src/core/Akka/Actor/Internal/ActorSystemImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
Expand Down Expand Up @@ -444,13 +445,25 @@ private void ConfigureProvider()
{
try
{
Type providerType = Type.GetType(_settings.ProviderClass);
if (providerType is null)
throw new ConfigurationException(
$"Could not resolve provider type [{_settings.ProviderClass}]. Ensure the type name is fully qualified.");
var provider =
(IActorRefProvider) Activator.CreateInstance(providerType, _name, _settings, _eventStream);
_provider = provider;
// The two built-in out-of-process providers are resolved from compile-time constant type
// names so the trimmer / Native AOT compiler can see which type each call site loads and
// keep it. Anything else is named only at runtime and stays fully dynamic.
_provider = _settings.ProviderSelectionType switch
{
ProviderSelection.Local => new LocalActorRefProvider(_name, _settings, _eventStream),
ProviderSelection.Remote => CreateProvider(
ProviderSelection.RemoteActorRefProvider,
"akka.actor.provider = remote, but Akka.Remote is not referenced by this application.",
_name, _settings, _eventStream),
ProviderSelection.Cluster => CreateProvider(
ProviderSelection.ClusterActorRefProvider,
"akka.actor.provider = cluster, but Akka.Cluster is not referenced by this application.",
_name, _settings, _eventStream),
_ => CreateProvider(
_settings.ProviderClass,
$"Could not resolve provider type [{_settings.ProviderClass}]. Ensure the type name is fully qualified.",
_name, _settings, _eventStream)
};
}
catch (Exception)
{
Expand All @@ -463,6 +476,33 @@ private void ConfigureProvider()
}
}

/// <summary>
/// Resolves an <see cref="IActorRefProvider"/> from its type name and constructs it.
/// </summary>
/// <remarks>
/// <paramref name="typeName"/> carries <see cref="DynamicallyAccessedMembersAttribute"/>, which is what
/// makes this trimmer-safe: the trimmer treats a <see cref="string"/> parameter annotated that way as a
/// type name, so a call site passing a compile-time constant - the <see cref="ProviderSelection"/>
/// constants for the built-in providers - has its type resolved statically and kept, along with the public
/// constructor this method invokes. The annotation flows into <see cref="Type.GetType(string)"/> here, so
/// neither that call nor the <see cref="Activator.CreateInstance(Type, object[])"/> below needs the
/// constant inlined in this method. A call site that passes a string only known at runtime cannot be
/// analyzed and warns there instead, which is the honest answer for a provider named only in HOCON.
/// </remarks>
private static IActorRefProvider CreateProvider(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] string typeName,
string notResolvedMessage,
string name,
Settings settings,
EventStream eventStream)
{
var providerType = Type.GetType(typeName);
if (providerType is null)
throw new ConfigurationException(notResolvedMessage);

return (IActorRefProvider)Activator.CreateInstance(providerType, name, settings, eventStream);
}

private void ConfigureLoggers()
{
_log = new BusLogging(_eventStream, "ActorSystem(" + _name + ")", GetType(), _settings.LogFormatter);
Expand Down
19 changes: 14 additions & 5 deletions src/core/Akka/Actor/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,20 @@ public Settings(ActorSystem system, Config config, ActorSystemSetup setup)
ProviderClass = ProviderSelectionType.Fqn;
HasCluster = ProviderSelectionType.HasCluster;

var providerType = Type.GetType(ProviderClass);
if (providerType == null)
throw new ConfigurationException($"'akka.actor.provider' is not a valid type name : '{ProviderClass}'");
if (!typeof(IActorRefProvider).IsAssignableFrom(providerType))
throw new ConfigurationException($"'akka.actor.provider' is not a valid actor ref provider: '{ProviderClass}'");
// The three built-in providers do not need to be validated here: their type names are
// compile-time constants and ActorSystemImpl.ConfigureProvider resolves each one from
// its own constant so the trimmer / Native AOT compiler can keep the type. Validating
// them through the ProviderClass property would reintroduce a dynamic Type.GetType call
// that the trimmer cannot see through (IL2057), and under Native AOT it would fail here
// even though the provider itself is perfectly resolvable.
if (ProviderSelectionType is ProviderSelection.Custom)

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

{
var providerType = Type.GetType(ProviderClass);
if (providerType == null)
throw new ConfigurationException($"'akka.actor.provider' is not a valid type name : '{ProviderClass}'");
if (!typeof(IActorRefProvider).IsAssignableFrom(providerType))
throw new ConfigurationException($"'akka.actor.provider' is not a valid actor ref provider: '{ProviderClass}'");
}

SupervisorStrategyClass = Config.GetString("akka.actor.guardian-supervisor-strategy", null);

Expand Down
Loading