Actor model and using of Akka.NET
Actor Model
At the same time when first object-oriented languages were emerging, another concept inspired by general relativity and quantum mechanics was taking shape – actor model. In general terms, the Actor model was defined in 1973 and was developed on a platform of multiple independent processors in a network. Similar to the object-oriented approach, this essentially mathematical model, revolved around the concept of actors. An actor is the smallest structural unit of the Actor model, and just like objects, they encapsulate data and behavior. Unlike objects, however, actors communicate with each other exclusively through messages. Messages in actors are processed in a serial manner. According to the full definition of actors, they can do three things:
- send a finite number of messages to other actors
- create a finite number of new actors
- designate the behavior to be used for the next message it receives
Some authors claim that actors are actually the most stringent form of objects. Let’s not forget that objects in Smalltalk-80 could hold state and send and receive messages, and that does sound awful like the definition of an actor. But apart from that, we can definitely see that there is a great benefit in this model, especially in concurrent, parallel processing environments and distributed systems. This is due to the fact that actors can affect each other only using messages, and by that, all locks are eliminated. Also, we can find a use for this concept in the rising world of microservices. We can consider that each microservice is, in fact, an actor in its own process.
What is great about this model is that we can apply best object-oriented practices on it. It seems that it is natural to use actors in combination with the Single responsibility principle, and make each actor do one thing (again pushing us to the concept of microservices). We should also notice the importance of messages. They are no longer just carriers of data, but also in a more abstract manner, carriers of behavior. What an actor will do depends on what message it received. This brings us to the fact that in actor systems, messages should be kept immutable, so they don’t change in the middle of processing and by that affect the behavior of the system. Also, this way race conditions would be minimal.
Another benefit of these systems is that they are inherently asynchronous. This can be considered a limitation, because synchronous behavior is harder to achieve.
Akka.NET
Akka is a toolkit which allows us to create an actor system in an efficient and simple way in .NET environment.
Configuration
To start with Akka.Net, you should first install the package in your project, using Package Manager Console:
Install-Package Akka
Also, to avoid the warning about deprecated serialization, install Hyperion package too:
Install-Package Akka.Serialization.Hyperion -pre
and add this to your App.config file:
<configSections><section name="akka" type="Akka.Configuration.Hocon.AkkaConfigurationSection, Akka" /> </configSections> <akka> <hocon> <![CDATA[ akka { actor { serializers { hyperion = "Akka.Serialization.HyperionSerializer, Akka.Serialization.Hyperion" } serialization-bindings { "System.Object" = hyperion } } ]]> </hocon> </akka>
Simple Use Case
When working with an actor system, the first thing we need to define is a message type which the actor will react to.
public class Message{ public string Text { get; private set; } public Message(string text) { Text = text; }}
Once that is defined, actor class can be created by implementing abstract ReceiveActor class:
public class MessageProcessor : ReceiveActor{ public MessageProcessor() { Receive<Message>(message => Console.WriteLine("Received a message with text: {0}", message.Text)); }}
And consume that actor like this:
public class Program{ static void Main(string[] arg) { var system = ActorSystem.Create("ActorSystem"); var actor = system.ActorOf<MessageProcessor>("actor"); actor.Tell(new Message("This is first message!")); Console.ReadLine(); }}
How about something more complicated
Ok, that was one easy example to get you started on how Akka works in general. But let’s consider something a little bit more complicated. Let’s make a system that will collect data about how long each reader was reading an article. The system will look like something like this:
It will contain the following actors:
- Blog Actor – Drives the whole system and receives messages from the simulated frontend. It will delegate messages to the rest of the system.
- Reporting Actor – Gathers data from users and the blog, and displays data to console.
- Users Actor – Parent of individual user actors, used to delegate messages to the correct user.
- User Actor – Calculates how much time the user has spent reading a certain article and forwards that information to the Reporting Actor.
In order to drive the whole system, there will be three types of messages:
- StartedReadingMessage – This message will indicate that a user started reading the article.
- StopedReadingMessage – This message will indicate that a user stopped reading the article.
- ReportingMessage – This message will be sent from User Actors
Since they all carry similar information there is a base Message class. Here is its implementation:
public abstract class Message { public string User { get; private set; } public string Article { get; private set; } public Message(string user, string article) { User = user; Article = article; } }
We can see that the base message contains information about the user and about the article. The rest of the messages is used for containing information about the action which is performed:
public class StartedReadingMessage : Message{ public StartedReadingMessage(string user, string article) : base(user, article) {}}class StopedReadingMessage : Message{ public StopedReadingMessage(string user, string article) : base (user, article) {}}public class ReportMessage : Message{ public long Milliseconds { get; private set; } public ReportMessage(string user, string article, long milliseconds) : base (user, article) { Milliseconds = milliseconds; }}
The main program drives this simulation by initializing the system as a whole and sending messages to the Blog Actor:
static void Main(string[] args){ ActorSystem system = ActorSystem.Create("rubikscode"); IActorRef blogActor = system.ActorOf(Props.Create(typeof(BlogActor)), "blog"); blogActor.Tell(new StartedReadingMessage("NapoleonHill", "Tuples in .NET world and C# 7.0 improvements")); // Used for simulation. Thread.Sleep(1000); blogActor.Tell(new StartedReadingMessage("VictorPelevin", "How to use “Art of War” to be better Software Craftsman")); // Used for simulation. Thread.Sleep(1000); blogActor.Tell(new StopedReadingMessage("NapoleonHill", "Tuples in .NET world and C# 7.0 improvements")); // Used for simulation. Thread.Sleep(500); blogActor.Tell(new StopedReadingMessage("VictorPelevin", "How to use “Art of War” to be better Software Craftsman")); Console.ReadLine();}
As mentioned before, the Blog Actor delegates messages to the rest of the actors. It is also in charge of creating the Users Actor and Reporting Actor. You may notice the use of the Context property of the actor, which is used for creating child actors. Also, there is the use of Props configuration class, which specifies options for the creation of actors.
public class BlogActor : ReceiveActor{ private IActorRef _users; private IActorRef _reporting; public BlogActor() { _users = Context.ActorOf(Props.Create(typeof(UsersActor)), "users"); _reporting = Context.ActorOf(Props.Create(typeof(ReportActor)), "reporting"); Receive<Message>(message => { _users.Forward(message); _reporting.Forward(message); }); }}
The Users Actor caches information about users, and routes messages to each individual User Actor.
public class UsersActor : ReceiveActor{ private Dictionary<string, IActorRef> _users; public UsersActor() { _users = new Dictionary<string, IActorRef>(); Receive<StartedReadingMessage>(message => ReceivedStartMessage(message)); Receive<StopedReadingMessage>(message => ReceivedStopMessage(message)); } private void ReceivedStartMessage(StartedReadingMessage message) { IActorRef userActor; if(!_users.TryGetValue(message.User, out userActor)) { userActor = Context.ActorOf(Props.Create(typeof(UserActor)), message.User); _users.Add(message.User, userActor); } userActor.Tell(message); } private void ReceivedStopMessage(StopedReadingMessage message) { IActorRef userActor; if (!_users.TryGetValue(message.User, out userActor)) { throw new InvalidOperationException("User doesn't exists!"); } userActor.Tell(message); }}
The implementation of the User Actor goes as follows:
public class UserActor : ReceiveActor{ private Stopwatch _stopwatch; private bool _isAlreadyReading; public UserActor() { _stopwatch = new Stopwatch(); Receive<StartedReadingMessage>(message => ReceivedStartMessage(message)); Receive<StopedReadingMessage>(message => ReceivedStopMessage(message)); } private void ReceivedStartMessage(StartedReadingMessage message) { if (_isAlreadyReading) throw new InvalidOperationException("User is already reading another article!"); _stopwatch.Start(); _isAlreadyReading = true; } private void ReceivedStopMessage(StopedReadingMessage message) { if (!_isAlreadyReading) throw new InvalidOperationException("User was not reading any article!"); _stopwatch.Stop(); _isAlreadyReading = false; Context.ActorSelection("../../reporting").Tell(new ReportMessage(message.User, message.Article, _stopwatch.ElapsedMilliseconds)); _stopwatch.Reset(); }}
And last but not least, here is the implementation of the Reporting Agent. It gets data from each individual User Actor, and from the Blog Actor, and calculates time spent on each blog post, and the number of views of each blog post.
public class ReportActor : ReceiveActor{ private Dictionary<string, long> _articleTimeSpent; private Dictionary<string, int> _articleViews; public ReportActor() { _articleTimeSpent = new Dictionary<string, long>(); _articleViews = new Dictionary<string, int>(); Receive<ReportMessage>(message => ReceivedReportMessage(message)); Receive<StartedReadingMessage>(message => IncreaseViewCounter(message)); } private void ReceivedReportMessage(ReportMessage message) { long time; if (_articleTimeSpent.TryGetValue(message.Article, out time)) time += message.Milliseconds; else _articleTimeSpent.Add(message.Article, message.Milliseconds); Console.WriteLine("******************************************************"); Console.WriteLine("User {0} was reading article {1} for {2} milliseconds.", message.User, message.Article, message.Milliseconds); Console.WriteLine("Aricle {0} was read for {1} milliseconds in total.", message.Article, _articleTimeSpent[message.Article]); Console.WriteLine("******************************************************\n"); } private void IncreaseViewCounter(StartedReadingMessage message) { int count; if (_articleViews.TryGetValue(message.Article, out count)) _articleViews[message.Article]++; else _articleViews.Add(message.Article, 1); Console.WriteLine("******************************************************"); Console.WriteLine("Article {0} has {1} views", message.Article, _articleViews[message.Article]); Console.WriteLine("******************************************************\n"); }}
This is how the result of this simulation looks like:
Conclusion
Actor Model gives us a different way of solving problems. Once you get into the message-driven mindset, you’ll find the Actor Model to be of great value when it comes to designing large-scale, service-oriented systems. On the other side, Akka.NET gives us a framework in which we can create these systems fairly easy. Here we have covered just basic uses of Akka.NET, but it has many more features that can help you..
If you need more info about the actor model, I recommend this video.
For further information about Akka.NET, you can visit their official site.