From 5bd0aacb6eb161fde5b4f094b435b56fa1e78012 Mon Sep 17 00:00:00 2001 From: Gregorius Soedharmo Date: Fri, 17 Jun 2022 00:32:25 +0700 Subject: [PATCH] Add Akka.Cluster.Tools.Client support (#66) * Add Akka.Cluster.Tools.Client support * Add settings unit tests * Simplify ClusterClient setup * Add some WithClusterClient overloads to give user options --- .../ClusterClientSpecs.cs | 39 ++++++ .../AkkaClusterHostingExtensions.cs | 130 ++++++++++++++++++ .../Properties/FriendsOf.cs | 3 + .../RemoteConfigurationSpecs.cs | 2 + 4 files changed, 174 insertions(+) create mode 100644 src/Akka.Cluster.Hosting.Tests/ClusterClientSpecs.cs create mode 100644 src/Akka.Cluster.Hosting/Properties/FriendsOf.cs diff --git a/src/Akka.Cluster.Hosting.Tests/ClusterClientSpecs.cs b/src/Akka.Cluster.Hosting.Tests/ClusterClientSpecs.cs new file mode 100644 index 00000000..acc8fee2 --- /dev/null +++ b/src/Akka.Cluster.Hosting.Tests/ClusterClientSpecs.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using Akka.Actor; +using Akka.Cluster.Tools.Client; +using FluentAssertions; +using FluentAssertions.Extensions; +using Xunit; + +namespace Akka.Cluster.Hosting.Tests; + +public class ClusterClientSpecs +{ + [Fact(DisplayName = "ClusterClientReceptionistSettings should be set correctly")] + public void ClusterClientReceptionistSettingsSpec() + { + var config = AkkaClusterHostingExtensions.CreateReceptionistConfig("customName", "customRole") + .GetConfig("akka.cluster.client.receptionist"); + var settings = ClusterReceptionistSettings.Create(config); + + config.GetString("name").Should().Be("customName"); + settings.Role.Should().Be("customRole"); + } + + [Fact(DisplayName = "ClusterClientSettings should be set correctly")] + public void ClusterClientSettingsSpec() + { + var contacts = new List + { + ActorPath.Parse("akka.tcp://one@localhost:1111/system/receptionist"), + ActorPath.Parse("akka.tcp://two@localhost:1111/system/receptionist"), + ActorPath.Parse("akka.tcp://three@localhost:1111/system/receptionist"), + }; + + var settings = AkkaClusterHostingExtensions.CreateClusterClientSettings( + ClusterClientReceptionist.DefaultConfig(), + contacts); + + settings.InitialContacts.Should().BeEquivalentTo(contacts); + } +} \ No newline at end of file diff --git a/src/Akka.Cluster.Hosting/AkkaClusterHostingExtensions.cs b/src/Akka.Cluster.Hosting/AkkaClusterHostingExtensions.cs index c6181ee9..0e2bef80 100644 --- a/src/Akka.Cluster.Hosting/AkkaClusterHostingExtensions.cs +++ b/src/Akka.Cluster.Hosting/AkkaClusterHostingExtensions.cs @@ -1,12 +1,18 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; +using System.Text; +using System.Text.RegularExpressions; using Akka.Actor; using Akka.Cluster.Sharding; using Akka.Cluster.Tools.Client; using Akka.Cluster.Tools.PublishSubscribe; using Akka.Cluster.Tools.Singleton; +using Akka.Configuration; using Akka.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace Akka.Cluster.Hosting { @@ -367,5 +373,129 @@ public static AkkaConfigurationBuilder WithSingletonProxy(this AkkaConfigu CreateAndRegisterSingletonProxy(singletonName, singletonManagerPath, singletonProxySettings, system, registry); }); } + + /// + /// Configures a for the + /// + /// The builder instance being configured. + /// Actor name of the ClusterReceptionist actor under the system path, by default it is /system/receptionist + /// Checks that the receptionist only start on members tagged with this role. All members are used if empty. + /// The same instance originally passed in. + public static AkkaConfigurationBuilder WithClusterClientReceptionist( + this AkkaConfigurationBuilder builder, + string name = "receptionist", + string role = null) + { + builder.AddHocon(CreateReceptionistConfig(name, role), HoconAddMode.Prepend); + return builder; + } + + internal static Config CreateReceptionistConfig(string name, string role) + { + const string root = "akka.cluster.client.receptionist."; + + var sb = new StringBuilder() + .Append(root).Append("name:").AppendLine(QuoteIfNeeded(name)); + + if(!string.IsNullOrEmpty(role)) + sb.Append(root).Append("role:").AppendLine(QuoteIfNeeded(role)); + + return ConfigurationFactory.ParseString(sb.ToString()); + } + + /// + /// Creates a and adds it to the using the given + /// . + /// + /// The builder instance being configured. + /// + /// List of that will be used as a seed + /// to discover all of the receptionists in the cluster. + /// + /// + /// This should look something like "akka.tcp://systemName@networkAddress:2552/system/receptionist" + /// + /// The key type to use for the . + /// The same instance originally passed in. + public static AkkaConfigurationBuilder WithClusterClient( + this AkkaConfigurationBuilder builder, + IList initialContacts) + { + if (initialContacts == null) + throw new ArgumentNullException(nameof(initialContacts)); + + if (initialContacts.Count < 1) + throw new ArgumentException("Must specify at least one initial contact", nameof(initialContacts)); + + return builder.WithActors((system, registry) => + { + var clusterClient = system.ActorOf(ClusterClient.Props( + CreateClusterClientSettings(system.Settings.Config, initialContacts))); + registry.TryRegister(clusterClient); + }); + } + + /// + /// Creates a and adds it to the using the given + /// . + /// + /// The builder instance being configured. + /// + /// List of node addresses where the are located that will be used as seed + /// to discover all of the receptionists in the cluster. + /// + /// + /// This should look something like "akka.tcp://systemName@networkAddress:2552" + /// + /// The name of the actor. + /// Defaults to "receptionist" + /// + /// The key type to use for the . + /// The same instance originally passed in. + public static AkkaConfigurationBuilder WithClusterClient( + this AkkaConfigurationBuilder builder, + IEnumerable
initialContactAddresses, + string receptionistActorName = "receptionist") + => builder.WithClusterClient(initialContactAddresses + .Select(address => new RootActorPath(address) / "system" / receptionistActorName) + .ToList()); + + /// + /// Creates a and adds it to the using the given + /// . + /// + /// The builder instance being configured. + /// + /// List of actor paths that will be used as a seed to discover all of the receptionists in the cluster. + /// + /// + /// This should look something like "akka.tcp://systemName@networkAddress:2552/system/receptionist" + /// + /// The key type to use for the . + /// The same instance originally passed in. + public static AkkaConfigurationBuilder WithClusterClient( + this AkkaConfigurationBuilder builder, + IEnumerable initialContacts) + => builder.WithClusterClient(initialContacts.Select(ActorPath.Parse).ToList()); + + internal static ClusterClientSettings CreateClusterClientSettings(Config config, IEnumerable initialContacts) + { + var clientConfig = config.GetConfig("akka.cluster.client"); + return ClusterClientSettings.Create(clientConfig) + .WithInitialContacts(initialContacts.ToImmutableHashSet()); + } + + #region Helper functions + + private static readonly Regex EscapeRegex = new Regex("[ \t:]{1}", RegexOptions.Compiled); + + private static string QuoteIfNeeded(string text) + { + return text == null + ? "" : EscapeRegex.IsMatch(text) + ? $"\"{text}\"" : text; + } + + #endregion } } diff --git a/src/Akka.Cluster.Hosting/Properties/FriendsOf.cs b/src/Akka.Cluster.Hosting/Properties/FriendsOf.cs new file mode 100644 index 00000000..0511f30e --- /dev/null +++ b/src/Akka.Cluster.Hosting/Properties/FriendsOf.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Akka.Cluster.Hosting.Tests")] \ No newline at end of file diff --git a/src/Akka.Remote.Hosting.Tests/RemoteConfigurationSpecs.cs b/src/Akka.Remote.Hosting.Tests/RemoteConfigurationSpecs.cs index b8fe0641..51e4f63a 100644 --- a/src/Akka.Remote.Hosting.Tests/RemoteConfigurationSpecs.cs +++ b/src/Akka.Remote.Hosting.Tests/RemoteConfigurationSpecs.cs @@ -29,4 +29,6 @@ public async Task AkkaRemoteShouldUsePublicHostnameCorrectly() // assert actorSystem.Provider.DefaultAddress.Host.Should().Be("localhost"); } + + } \ No newline at end of file