Skip to content

Commit 5bd0aac

Browse files
authored
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
1 parent 9c2385a commit 5bd0aac

File tree

4 files changed

+174
-0
lines changed

4 files changed

+174
-0
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System.Collections.Generic;
2+
using Akka.Actor;
3+
using Akka.Cluster.Tools.Client;
4+
using FluentAssertions;
5+
using FluentAssertions.Extensions;
6+
using Xunit;
7+
8+
namespace Akka.Cluster.Hosting.Tests;
9+
10+
public class ClusterClientSpecs
11+
{
12+
[Fact(DisplayName = "ClusterClientReceptionistSettings should be set correctly")]
13+
public void ClusterClientReceptionistSettingsSpec()
14+
{
15+
var config = AkkaClusterHostingExtensions.CreateReceptionistConfig("customName", "customRole")
16+
.GetConfig("akka.cluster.client.receptionist");
17+
var settings = ClusterReceptionistSettings.Create(config);
18+
19+
config.GetString("name").Should().Be("customName");
20+
settings.Role.Should().Be("customRole");
21+
}
22+
23+
[Fact(DisplayName = "ClusterClientSettings should be set correctly")]
24+
public void ClusterClientSettingsSpec()
25+
{
26+
var contacts = new List<ActorPath>
27+
{
28+
ActorPath.Parse("akka.tcp://one@localhost:1111/system/receptionist"),
29+
ActorPath.Parse("akka.tcp://two@localhost:1111/system/receptionist"),
30+
ActorPath.Parse("akka.tcp://three@localhost:1111/system/receptionist"),
31+
};
32+
33+
var settings = AkkaClusterHostingExtensions.CreateClusterClientSettings(
34+
ClusterClientReceptionist.DefaultConfig(),
35+
contacts);
36+
37+
settings.InitialContacts.Should().BeEquivalentTo(contacts);
38+
}
39+
}

src/Akka.Cluster.Hosting/AkkaClusterHostingExtensions.cs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Collections.Immutable;
34
using System.Linq;
5+
using System.Text;
6+
using System.Text.RegularExpressions;
47
using Akka.Actor;
58
using Akka.Cluster.Sharding;
69
using Akka.Cluster.Tools.Client;
710
using Akka.Cluster.Tools.PublishSubscribe;
811
using Akka.Cluster.Tools.Singleton;
12+
using Akka.Configuration;
913
using Akka.Hosting;
14+
using Microsoft.Extensions.DependencyInjection;
15+
using Microsoft.Extensions.DependencyInjection.Extensions;
1016

1117
namespace Akka.Cluster.Hosting
1218
{
@@ -367,5 +373,129 @@ public static AkkaConfigurationBuilder WithSingletonProxy<TKey>(this AkkaConfigu
367373
CreateAndRegisterSingletonProxy<TKey>(singletonName, singletonManagerPath, singletonProxySettings, system, registry);
368374
});
369375
}
376+
377+
/// <summary>
378+
/// Configures a <see cref="ClusterClientReceptionist"/> for the <see cref="ActorSystem"/>
379+
/// </summary>
380+
/// <param name="builder">The builder instance being configured.</param>
381+
/// <param name="name">Actor name of the ClusterReceptionist actor under the system path, by default it is /system/receptionist</param>
382+
/// <param name="role">Checks that the receptionist only start on members tagged with this role. All members are used if empty.</param>
383+
/// <returns>The same <see cref="AkkaConfigurationBuilder"/> instance originally passed in.</returns>
384+
public static AkkaConfigurationBuilder WithClusterClientReceptionist(
385+
this AkkaConfigurationBuilder builder,
386+
string name = "receptionist",
387+
string role = null)
388+
{
389+
builder.AddHocon(CreateReceptionistConfig(name, role), HoconAddMode.Prepend);
390+
return builder;
391+
}
392+
393+
internal static Config CreateReceptionistConfig(string name, string role)
394+
{
395+
const string root = "akka.cluster.client.receptionist.";
396+
397+
var sb = new StringBuilder()
398+
.Append(root).Append("name:").AppendLine(QuoteIfNeeded(name));
399+
400+
if(!string.IsNullOrEmpty(role))
401+
sb.Append(root).Append("role:").AppendLine(QuoteIfNeeded(role));
402+
403+
return ConfigurationFactory.ParseString(sb.ToString());
404+
}
405+
406+
/// <summary>
407+
/// Creates a <see cref="ClusterClient"/> and adds it to the <see cref="ActorRegistry"/> using the given
408+
/// <see cref="TKey"/>.
409+
/// </summary>
410+
/// <param name="builder">The builder instance being configured.</param>
411+
/// <param name="initialContacts"> <para>
412+
/// List of <see cref="ClusterClientReceptionist"/> <see cref="ActorPath"/> that will be used as a seed
413+
/// to discover all of the receptionists in the cluster.
414+
/// </para>
415+
/// <para>
416+
/// This should look something like "akka.tcp://systemName@networkAddress:2552/system/receptionist"
417+
/// </para></param>
418+
/// <typeparam name="TKey">The key type to use for the <see cref="ActorRegistry"/>.</typeparam>
419+
/// <returns>The same <see cref="AkkaConfigurationBuilder"/> instance originally passed in.</returns>
420+
public static AkkaConfigurationBuilder WithClusterClient<TKey>(
421+
this AkkaConfigurationBuilder builder,
422+
IList<ActorPath> initialContacts)
423+
{
424+
if (initialContacts == null)
425+
throw new ArgumentNullException(nameof(initialContacts));
426+
427+
if (initialContacts.Count < 1)
428+
throw new ArgumentException("Must specify at least one initial contact", nameof(initialContacts));
429+
430+
return builder.WithActors((system, registry) =>
431+
{
432+
var clusterClient = system.ActorOf(ClusterClient.Props(
433+
CreateClusterClientSettings(system.Settings.Config, initialContacts)));
434+
registry.TryRegister<TKey>(clusterClient);
435+
});
436+
}
437+
438+
/// <summary>
439+
/// Creates a <see cref="ClusterClient"/> and adds it to the <see cref="ActorRegistry"/> using the given
440+
/// <see cref="TKey"/>.
441+
/// </summary>
442+
/// <param name="builder">The builder instance being configured.</param>
443+
/// <param name="initialContactAddresses"> <para>
444+
/// List of node addresses where the <see cref="ClusterClientReceptionist"/> are located that will be used as seed
445+
/// to discover all of the receptionists in the cluster.
446+
/// </para>
447+
/// <para>
448+
/// This should look something like "akka.tcp://systemName@networkAddress:2552"
449+
/// </para></param>
450+
/// <param name="receptionistActorName">The name of the <see cref="ClusterClientReceptionist"/> actor.
451+
/// Defaults to "receptionist"
452+
/// </param>
453+
/// <typeparam name="TKey">The key type to use for the <see cref="ActorRegistry"/>.</typeparam>
454+
/// <returns>The same <see cref="AkkaConfigurationBuilder"/> instance originally passed in.</returns>
455+
public static AkkaConfigurationBuilder WithClusterClient<TKey>(
456+
this AkkaConfigurationBuilder builder,
457+
IEnumerable<Address> initialContactAddresses,
458+
string receptionistActorName = "receptionist")
459+
=> builder.WithClusterClient<TKey>(initialContactAddresses
460+
.Select(address => new RootActorPath(address) / "system" / receptionistActorName)
461+
.ToList());
462+
463+
/// <summary>
464+
/// Creates a <see cref="ClusterClient"/> and adds it to the <see cref="ActorRegistry"/> using the given
465+
/// <see cref="TKey"/>.
466+
/// </summary>
467+
/// <param name="builder">The builder instance being configured.</param>
468+
/// <param name="initialContacts"> <para>
469+
/// List of actor paths that will be used as a seed to discover all of the receptionists in the cluster.
470+
/// </para>
471+
/// <para>
472+
/// This should look something like "akka.tcp://systemName@networkAddress:2552/system/receptionist"
473+
/// </para></param>
474+
/// <typeparam name="TKey">The key type to use for the <see cref="ActorRegistry"/>.</typeparam>
475+
/// <returns>The same <see cref="AkkaConfigurationBuilder"/> instance originally passed in.</returns>
476+
public static AkkaConfigurationBuilder WithClusterClient<TKey>(
477+
this AkkaConfigurationBuilder builder,
478+
IEnumerable<string> initialContacts)
479+
=> builder.WithClusterClient<TKey>(initialContacts.Select(ActorPath.Parse).ToList());
480+
481+
internal static ClusterClientSettings CreateClusterClientSettings(Config config, IEnumerable<ActorPath> initialContacts)
482+
{
483+
var clientConfig = config.GetConfig("akka.cluster.client");
484+
return ClusterClientSettings.Create(clientConfig)
485+
.WithInitialContacts(initialContacts.ToImmutableHashSet());
486+
}
487+
488+
#region Helper functions
489+
490+
private static readonly Regex EscapeRegex = new Regex("[ \t:]{1}", RegexOptions.Compiled);
491+
492+
private static string QuoteIfNeeded(string text)
493+
{
494+
return text == null
495+
? "" : EscapeRegex.IsMatch(text)
496+
? $"\"{text}\"" : text;
497+
}
498+
499+
#endregion
370500
}
371501
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
using System.Runtime.CompilerServices;
2+
3+
[assembly: InternalsVisibleTo("Akka.Cluster.Hosting.Tests")]

src/Akka.Remote.Hosting.Tests/RemoteConfigurationSpecs.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,6 @@ public async Task AkkaRemoteShouldUsePublicHostnameCorrectly()
2929
// assert
3030
actorSystem.Provider.DefaultAddress.Host.Should().Be("localhost");
3131
}
32+
33+
3234
}

0 commit comments

Comments
 (0)