diff --git a/Nixie.Tests/Actors/RouteeActorStruct.cs b/Nixie.Tests/Actors/RouteeActorStruct.cs new file mode 100644 index 0000000..d00c025 --- /dev/null +++ b/Nixie.Tests/Actors/RouteeActorStruct.cs @@ -0,0 +1,60 @@ + +using Nixie.Routers; + +namespace Nixie.Tests.Actors; + +public readonly struct RouterMessageStruct : IConsistentHashable +{ + public RouterMessageType Type { get; } + + public string Data { get; } + + public RouterMessageStruct(RouterMessageType type, string data) + { + Type = type; + Data = data; + } + + public int GetHash() + { + return Data switch + { + "aaa" => 0, + "bbb" => 1, + "ccc" => 2, + "ddd" => 3, + "eee" => 4, + _ => 0 + }; + } +} + +public sealed class RouteeActorStruct : IActorStruct +{ + private int receivedMessages; + + private readonly IActorContextStruct context; + + public RouteeActorStruct(IActorContextStruct context) + { + this.context = context; + } + + public int GetMessages() + { + return receivedMessages; + } + + private void IncrMessage() + { + receivedMessages++; + } + + public async Task Receive(RouterMessageStruct message) + { + await Task.Yield(); + + if (message.Type == RouterMessageType.Route) + IncrMessage(); + } +} diff --git a/Nixie.Tests/TestRouters.cs b/Nixie.Tests/TestRouters.cs index 76d1041..af5f3e4 100644 --- a/Nixie.Tests/TestRouters.cs +++ b/Nixie.Tests/TestRouters.cs @@ -35,6 +35,35 @@ public async Task TestCreateRoundRobinRouter() Assert.Equal(1, routeeActor.GetMessages()); } } + + [Fact] + public async Task TestCreateRoundRobinRouterStruct() + { + using ActorSystem asx = new(); + + IActorRefStruct, RouterMessageStruct> router = + asx.SpawnStruct, RouterMessageStruct>("my-router", 5); + + router.Send(new RouterMessageStruct(RouterMessageType.Route, "aaa")); + router.Send(new RouterMessageStruct(RouterMessageType.Route, "bbb")); + router.Send(new RouterMessageStruct(RouterMessageType.Route, "ccc")); + router.Send(new RouterMessageStruct(RouterMessageType.Route, "ddd")); + router.Send(new RouterMessageStruct(RouterMessageType.Route, "eee")); + + await asx.Wait(); + + Assert.IsAssignableFrom>(router.Runner.Actor); + + RoundRobinActorStruct routerActor = (RoundRobinActorStruct)router.Runner.Actor!; + + foreach (IActorRefStruct routee in routerActor.Instances) + { + Assert.IsAssignableFrom(routee.Runner.Actor); + + RouteeActorStruct routeeActor = (RouteeActorStruct)routee.Runner.Actor!; + Assert.Equal(1, routeeActor.GetMessages()); + } + } [Fact] public async Task TestCreateRoundRobinRouterExt() diff --git a/Nixie/ActorRepositoryReply.cs b/Nixie/ActorRepositoryReply.cs index 5d75988..4319296 100644 --- a/Nixie/ActorRepositoryReply.cs +++ b/Nixie/ActorRepositoryReply.cs @@ -75,7 +75,7 @@ public bool IsProcessing(out string? actorName) { ActorRunner runner = lazyValue.Value.runner; - if (!runner.IsShutdown && runner.IsProcessing) + if (runner is { IsShutdown: false, IsProcessing: true }) { actorName = runner.Name; return true; @@ -110,7 +110,7 @@ public IActorRef Spawn(string? name = null, params Lazy<(ActorRunner runner, ActorRef actorRef)> actor = actors.GetOrAdd( name, - (string name) => new Lazy<(ActorRunner, ActorRef)>(() => CreateInternal(name, args)) + (string _) => new(() => CreateInternal(name, args)) ); return actor.Value.actorRef; diff --git a/Nixie/Nixie.csproj b/Nixie/Nixie.csproj index 0c3b7b7..615d59a 100644 --- a/Nixie/Nixie.csproj +++ b/Nixie/Nixie.csproj @@ -9,7 +9,7 @@ enable enable Nixie - 1.0.3 + 1.0.4 A Lightweight Actor Model Implementation for C#/.NET Andres Gutierrez Andres Gutierrez diff --git a/Nixie/Routers/ActorSystemExtensions.cs b/Nixie/Routers/ActorSystemExtensions.cs index cd5ffec..99dda82 100644 --- a/Nixie/Routers/ActorSystemExtensions.cs +++ b/Nixie/Routers/ActorSystemExtensions.cs @@ -159,7 +159,7 @@ List> instances } /// - /// Creates a Round-Robin router specifying number of instances + /// Creates a Consistent Hash router specifying name and number of instances /// /// /// diff --git a/Nixie/Routers/ActorSystemExtensionsStruct.cs b/Nixie/Routers/ActorSystemExtensionsStruct.cs new file mode 100644 index 0000000..662a308 --- /dev/null +++ b/Nixie/Routers/ActorSystemExtensionsStruct.cs @@ -0,0 +1,290 @@ + +namespace Nixie.Routers; + +public static class ActorSystemExtensionsStruct +{ + /// + /// Creates a Round-Robin router specifying name and number of instances + /// + /// + /// + /// + /// + /// + /// + public static IActorRefStruct, TRequest> CreateRoundRobinRouterStruct( + this ActorSystem actorSystem, + string name, + int instances + ) + where TActor : IActorStruct where TRequest : struct + { + return actorSystem.SpawnStruct, TRequest>(name, instances); + } + + /// + /// Creates a Round-Robin router specifying number of instances + /// + /// + /// + /// + /// + /// + public static IActorRefStruct, TRequest> CreateRoundRobinRouterStruct( + this ActorSystem actorSystem, + int instances + ) + where TActor : IActorStruct where TRequest : struct + { + return actorSystem.SpawnStruct, TRequest>(null, instances); + } + + /// + /// Creates a Round-Robin router specifying name and a list of routee actors + /// + /// + /// + /// + /// + /// + /// + public static IActorRefStruct, TRequest> CreateRoundRobinRouterStruct( + this ActorSystem actorSystem, + string name, + List> instances + ) + where TActor : IActorStruct where TRequest : struct + { + return actorSystem.SpawnStruct, TRequest>(name, instances); + } + + /// + /// Creates a Round-Robin router specifying a list of routee actors7uh + /// + /// + /// + /// + /// + /// + public static IActorRefStruct, TRequest> CreateRoundRobinRouterStruct( + this ActorSystem actorSystem, + List> instances + ) + where TActor : IActorStruct where TRequest : struct + { + return actorSystem.SpawnStruct, TRequest>(null, instances); + } + + /// + /// Creates a Round-Robin router specifying name and number of instances + /// + /// + /// + /// + /// + /// + /// + /// + public static IActorRefStruct, TRequest, TResponse> + CreateRoundRobinRouterStruct( + this ActorSystem actorSystem, + string name, + int instances + ) + where TActor : IActorStruct where TRequest : struct where TResponse : struct + { + return actorSystem.SpawnStruct, TRequest, TResponse>(name, instances); + } + + /// + /// Creates a Round-Robin router specifying name and number of instances + /// + /// + /// + /// + /// + /// + /// + public static IActorRefStruct, TRequest, TResponse> + CreateRoundRobinRouter( + this ActorSystem actorSystem, + int instances + ) + where TActor : IActorStruct where TRequest : struct where TResponse : struct + { + return actorSystem.SpawnStruct, TRequest, TResponse>(null, instances); + } + + /// + /// Creates a Round-Robin router specifying name and a list of instances + /// + /// + /// + /// + /// + /// + /// + /// + public static IActorRefStruct, TRequest, TResponse> + CreateRoundRobinRouterStruct( + this ActorSystem actorSystem, + string name, + List> instances + ) + where TActor : IActorStruct where TRequest : struct where TResponse : struct + { + return actorSystem.SpawnStruct, TRequest, TResponse>(name, instances); + } + + /// + /// Creates a Round-Robin router specifying name and a list of instances + /// + /// + /// + /// + /// + /// + /// + public static IActorRefStruct, TRequest, TResponse> + CreateRoundRobinRouterStruct( + this ActorSystem actorSystem, + List> instances + ) + where TActor : IActorStruct where TRequest : struct where TResponse : struct + { + return actorSystem.SpawnStruct, TRequest, TResponse>(null, instances); + } + + /// + /// Creates a Consistent Hash router specifying name and number of instances + /// + /// + /// + /// + /// + /// + /// + public static IActorRefStruct, TRequest> CreateConsistentHashRouterStruct( + this ActorSystem actorSystem, + string name, + int instances + ) + where TActor : IActorStruct where TRequest : struct, IConsistentHashable + { + return actorSystem.SpawnStruct, TRequest>(name, instances); + } + + /// + /// Creates a Consistent Hash router specifying name and number of instances + /// + /// + /// + /// + /// + /// + public static IActorRefStruct, TRequest> CreateConsistentHashRouterStruct( + this ActorSystem actorSystem, + int instances + ) + where TActor : IActorStruct where TRequest : struct, IConsistentHashable + { + return actorSystem.SpawnStruct, TRequest>(null, instances); + } + + /// + /// Creates a Consistent Hash router specifying name and number of instances + /// + /// + /// + /// + /// + /// + /// + public static IActorRefStruct, TRequest, TResponse> + CreateConsistentHashRouterStruct( + this ActorSystem actorSystem, + int instances + ) + where TActor : IActorStruct where TRequest : struct, IConsistentHashable where TResponse : struct + { + return actorSystem.SpawnStruct, TRequest, TResponse>(null, instances); + } + + /// + /// Creates a Consistent Hash router specifying name and a list of instances + /// + /// + /// + /// + /// + /// + public static IActorRefStruct, TRequest> CreateConsistentHashRouterStruct( + this ActorSystem actorSystem, + List> instances + ) + where TActor : IActorStruct where TRequest : struct, IConsistentHashable + { + return actorSystem.SpawnStruct, TRequest>(null, instances); + } + + /// + /// Creates a Consistent Hash router specifying name and number of instances + /// + /// + /// + /// + /// + /// + /// + /// + public static IActorRefStruct, TRequest, TResponse> + CreateConsistentHashRouterStruct( + this ActorSystem actorSystem, + string name, + int instances + ) + where TActor : IActorStruct where TRequest : struct, IConsistentHashable where TResponse : struct + { + return actorSystem.SpawnStruct, TRequest, TResponse>(name, instances); + } + + /// + /// Creates a Consistent Hash router specifying name and a list of instances + /// + /// + /// + /// + /// + /// + /// + /// + public static IActorRefStruct, TRequest, TResponse> + CreateConsistentHashRouterStruct( + this ActorSystem actorSystem, + string name, + List> instances + ) + where TActor : IActorStruct where TRequest : struct, IConsistentHashable where TResponse : struct + { + return actorSystem.SpawnStruct, TRequest, TResponse>(name, instances); + } + + /// + /// Creates a Consistent Hash router specifying name and a list of instances + /// + /// + /// + /// + /// + /// + /// + public static IActorRefStruct, TRequest, TResponse> + CreateConsistentHashRouterStruct( + this ActorSystem actorSystem, + List> instances + ) + where TActor : IActorStruct where TRequest : struct, IConsistentHashable where TResponse : struct + { + return actorSystem.SpawnStruct, TRequest, TResponse>(null, instances); + } +} \ No newline at end of file diff --git a/Nixie/Routers/ConsistentHashActorStruct.cs b/Nixie/Routers/ConsistentHashActorStruct.cs new file mode 100644 index 0000000..5d245db --- /dev/null +++ b/Nixie/Routers/ConsistentHashActorStruct.cs @@ -0,0 +1,57 @@ + +namespace Nixie.Routers; + +/// +/// Utilizes consistent hashing to choose a routee based on the transmitted message. +/// +/// +/// +public class ConsistentHashActorStruct : IActorStruct + where TActor : IActorStruct where TRequest : struct, IConsistentHashable +{ + private readonly IActorContextStruct, TRequest> context; + + private readonly List> instances = new(); + + /// + /// Returns the list of instances + /// + public List> Instances => instances; + + /// + /// Constructor + /// + /// + /// + public ConsistentHashActorStruct(IActorContextStruct, TRequest> context, int numberInstances) + { + this.context = context; + + for (int i = 0; i < numberInstances; i++) + instances.Add(context.ActorSystem.SpawnStruct()); + } + + /// + /// Constructor + /// + /// + /// + public ConsistentHashActorStruct(IActorContextStruct, TRequest> context, List> instances) + { + this.context = context; + this.instances = instances; + } + + /// + /// Receives a message that must be routed to one of the routees + /// + /// + /// + public Task Receive(TRequest message) + { + IActorRefStruct instance = instances[message.GetHash() % instances.Count]; + instance.Send(message); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Nixie/Routers/ConsistentHashActorStructReply.cs b/Nixie/Routers/ConsistentHashActorStructReply.cs new file mode 100644 index 0000000..f74a106 --- /dev/null +++ b/Nixie/Routers/ConsistentHashActorStructReply.cs @@ -0,0 +1,58 @@ + +namespace Nixie.Routers; + +/// +/// Utilizes consistent hashing to choose a routee based on the transmitted message. +/// +/// +/// +public class ConsistentHashActorStruct : IActorStruct + where TActor : IActorStruct where TRequest : struct, IConsistentHashable where TResponse : struct +{ + private readonly IActorContextStruct, TRequest, TResponse> context; + + private readonly List> instances = new(); + + /// + /// Returns the list of instances + /// + public List> Instances => instances; + + /// + /// Constructor + /// + /// + /// + public ConsistentHashActorStruct(IActorContextStruct, TRequest, TResponse> context, int numberInstances) + { + this.context = context; + + for (int i = 0; i < numberInstances; i++) + instances.Add(context.ActorSystem.SpawnStruct()); + } + + /// + /// Constructor + /// + /// + /// + public ConsistentHashActorStruct(IActorContextStruct, TRequest, TResponse> context, List> instances) + { + this.context = context; + this.instances = instances; + } + + /// + /// Receives a message that must be routed to one of the routees + /// + /// + /// + public Task Receive(TRequest message) + { + int bucket = Math.Abs(message.GetHash()) % instances.Count; + IActorRefStruct instance = instances[bucket]; + context.ByPassReply = true; // Marks the response to be bypassed so other actor can reply + instance.Send(message, context.Reply); + return Task.FromResult((TResponse)default); + } +} diff --git a/Nixie/Routers/RoundRobinActorReply.cs b/Nixie/Routers/RoundRobinActorReply.cs index 7040e21..e52e00e 100644 --- a/Nixie/Routers/RoundRobinActorReply.cs +++ b/Nixie/Routers/RoundRobinActorReply.cs @@ -6,7 +6,7 @@ namespace Nixie.Routers; /// each actor receives one message. /// /// The round robin method offers equitable distribution, with each routee receiving an equal number of messages -/// when the group remains relatively constant.However, if there are frequent changes in the group of routees, +/// when the group remains relatively constant. However, if there are frequent changes in the group of routees, /// the distribution might not be as balanced. /// /// diff --git a/Nixie/Routers/RoundRobinActorStruct.cs b/Nixie/Routers/RoundRobinActorStruct.cs new file mode 100644 index 0000000..594ca6a --- /dev/null +++ b/Nixie/Routers/RoundRobinActorStruct.cs @@ -0,0 +1,68 @@ +namespace Nixie.Routers; + +/// +/// The router cycles through the group of routees, ensuring that for every n messages sent, +/// each actor receives one message. +/// +/// The round robin method offers equitable distribution, with each routee receiving an equal number of messages +/// when the group remains relatively constant.However, if there are frequent changes in the group of routees, +/// the distribution might not be as balanced. +/// +/// +/// +public class RoundRobinActorStruct : IActorStruct + where TActor : IActorStruct where TRequest : struct +{ + private int position = -1; + + private readonly IActorContextStruct, TRequest> context; + + private readonly List> instances = new(); + + /// + /// Returns the current list of instances + /// + public List> Instances => instances; + + /// + /// Constructor + /// + /// + /// + public RoundRobinActorStruct(IActorContextStruct, TRequest> context, int numberInstances) + { + this.context = context; + + for (int i = 0; i < numberInstances; i++) + instances.Add(context.ActorSystem.SpawnStruct()); + } + + /// + /// Constructor + /// + /// + /// + public RoundRobinActorStruct(IActorContextStruct, TRequest> context, List> instances) + { + this.context = context; + this.instances = instances; + } + + /// + /// Receives a message that must be routed to one of the routees + /// + /// + /// + public Task Receive(TRequest message) + { + if ((position + 1) >= int.MaxValue) + position = 0; + else + position++; + + IActorRefStruct instance = instances[position % instances.Count]; + instance.Send(message); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Nixie/Routers/RoundRobinActorStructReply.cs b/Nixie/Routers/RoundRobinActorStructReply.cs new file mode 100644 index 0000000..23ac633 --- /dev/null +++ b/Nixie/Routers/RoundRobinActorStructReply.cs @@ -0,0 +1,69 @@ +namespace Nixie.Routers; + +/// +/// The router cycles through the group of routees, ensuring that for every n messages sent, +/// each actor receives one message. +/// +/// The round robin method offers equitable distribution, with each routee receiving an equal number of messages +/// when the group remains relatively constant. However, if there are frequent changes in the group of routees, +/// the distribution might not be as balanced. +/// +/// +/// +/// +public class RoundRobinActorStruct : IActorStruct + where TActor : IActorStruct where TRequest : struct where TResponse : struct +{ + private int position = -1; + + private readonly IActorContextStruct, TRequest, TResponse> context; + + private readonly List> instances = new(); + + /// + /// Returns the current list of instances + /// + public List> Instances => instances; + + /// + /// Constructor + /// + /// + /// + public RoundRobinActorStruct(IActorContextStruct, TRequest, TResponse> context, int numberInstances) + { + this.context = context; + + for (int i = 0; i < numberInstances; i++) + instances.Add(context.ActorSystem.SpawnStruct()); + } + + /// + /// Constructor + /// + /// + /// + public RoundRobinActorStruct(IActorContextStruct, TRequest, TResponse> context, List> instances) + { + this.context = context; + this.instances = instances; + } + + /// + /// Receives a message that must be routed to one of the routees + /// + /// + /// + public Task Receive(TRequest message) + { + if ((position + 1) >= int.MaxValue) + position = 0; + else + position++; + + IActorRefStruct instance = instances[position % instances.Count]; + context.ByPassReply = true; // Marks the response to be bypassed so other actor can reply + instance.Send(message, context.Reply); + return Task.FromResult((TResponse)default); + } +} \ No newline at end of file diff --git a/README.md b/README.md index b37d5a7..2fedde2 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ To install Nixie into your C#/.NET project, you can use the .NET CLI or the NuGe #### Using .NET CLI ```shell -dotnet add package Nixie --version 1.0.3 +dotnet add package Nixie --version 1.0.4 ``` ### Using NuGet Package Manager @@ -43,7 +43,7 @@ dotnet add package Nixie --version 1.0.3 Search for Nixie and install it from the NuGet package manager UI, or use the Package Manager Console: ```shell -Install-Package Nixie -Version 1.0.3 +Install-Package Nixie -Version 1.0.4 ``` ## Usage