This text is a summary of a webinar originally hosted by Mateusz Strycharski, Senior Engineer at Zartis. You can watch the full recording here.
In this article, we will be taking a look at Actor Model systems.
We’ll start by explaining what an actor is and what it can do. The discussion will then go into the history of actor model systems, how they differ from rival solutions, and some potential scenarios that may be a good fit for this model.
We will also review one specific actor system, Akka.NET, and go over the hierarchy and communication between actors — perhaps the two most important aspects of actor systems.
What Is the Actor Model?
The so-called actor model is a mathematical model of concurrent computation that first entered the tech scene as a theoretical concept in 1973. It operates by setting down general guidelines on how system components — called actors — should interact in a concurrent computational environment. The two best-known implementations of the actor model are Akka and Erlang.
The actor model allows for a higher level of abstraction when writing concurrent and distributed systems. It spares the developer from having to deal with explicit locking and thread management, making it easier to write concurrent and parallel systems.
At this point, you may be wondering whether we needed yet another concurrency model in the first place. Didn’t .NET already have its own concurrency threading model?
Well, not quite. You see, handling concurrency issues in the .NET world can be pretty challenging. It requires multi-threading code, which is hard to write, test, and debug.
One of the advantages of the actor model is that its universal primitive is the actor. To achieve concurrency, you need to have at least a few actors that communicate with each other.
You can think of an actor as an object in memory — but bear in mind that it differs from the usual objects you may be thinking of. Like every object, an actor has a state and behavior. What sets it apart, however, is its mailbox, which is the only means of communication with the actor. There is no way, for example, to read the actor state apart from sending a message to the mailbox and triggering messages to be sent back to you.
What’s important to note here is that each actor is only able to handle one message at a time. If you take a look at the picture on the left, you’ll see that while there are several messages in the mailbox, the actor processes them one by one.
At first glance, this setup may strike you as rather odd. Doesn’t handling just one message at a time go against the very concept of concurrent systems?
Actor systems manage to achieve effective concurrency by splitting the work between as many and as small actors as possible. In the image above, you can see that the actor has a parent actor and child actors P1, P2, and P3. Each actor in a system can create multiple child actors to distribute the work more effectively.
What Can Actors Do for Your System?
In addition to creating child actors, actors handle messages from the mailbox. For each message it receives, an actor has to decide whether to:
- Update the internal state
- Perform specific actions, such as communicating externally through APIS, writing to DB, or sending HTTP requests
- Send a new message to a different actor
- Respond to the sender 0 or more times
What Is an Actor System?
To put it simply, an actor system is the place where an actor lives. In the case of Akka.NET, the actor system is provided by the framework.
You can use actor systems to manage things such as:
- Actor lifetime cycle
From a development perspective, it’s nice to have an actor system that is transparent and easily traceable, as that can help save time when trying to diagnose problems later on.
Let’s consider a practical example. In the screenshot below, you can see a reservation actor for hotel room bookings created using Akka.NET. The actor is C# class, which inherits from one of the classes provided by the framework:
The actor in this example is a receive actor, which means you’ll have to define what types of messages it will be handling.
As you can see, the actor will receive three types of messages:
To define these, you need to call the Receive (it is typed method) in the constructor of the actor class. The template parameter determines the type of message that will be handled by the actor. As a parameter Receive accepts a function that, in turn, defines what happens when there is an incoming message of a particular type.
Note that when an actor receives a message with no defined function, the message will be ignored. For instance, if the actor receives a Type A message, it will disregard it.
The example above can also help showcase how actors handle the concurrency problem.
Imagine that two people are trying to book the same room at the same time. In our example, we can achieve that by sending two BookTheRoom messages for Room 1 simultaneously. The messages will be queued in the mailbox, and the reservation actor will handle them one by one. That eliminates the need to worry about locking the shared state of the actor using mutexes, for example.
How to Create an Actor System in Akka.NET
To create an actor system, we call the static method Create on the ActorSystem class. The ActorSystem class is provided by the framework. In order to call this method you need to pass the name of the system (see the first line of code below). Once you have created the system, you can start creating actors.
It’s worth mentioning that this actor method returns IActorRef, which is a proxy. This is to ensure that no one will be able to access the actor state.
As seen above, we can send a message to the actor using the Tell method. When the message is sent, the function in Receive will be executed.
Next, let’s imagine that the available room from our example has been booked successfully. We would now like to send the same message to, say, a billing actor. By creating child actors and queues in the reservation actor, we can send a message to the child actors using the context property. To create child actors, you will always need to use the context property provided by the framework. See below:
Hierarchy and Communication Between Actors
As we mentioned earlier, the actors in a system will form a hierarchy. The main actor that sits at the top of the hierarchy is provided by the framework. In this case, the framework is Akka.NET. This actor will be the parent of all other actors you may create. So, even if you create first-level actors, the parent will still be the main actor as provided by the framework.
Hierarchy Between Actors
There are two methods for communication with an actor.
Each actor in a hierarchy has a unique identifying address. An actor can communicate directly with other actors if it knows their respective addresses.
In the illustration on the left, actor C1 can communicate with actor A1 if it knows its address even though the latter is not a child of actor C.
Akka.NET provides location transparency, which means that where the actor lives is not important as long as you know its address. This is useful because it allows you to distribute actors across different machines without any problems.
The second way to communicate is to use the context properties for actor A to gain access to the sender or the parent.
Communication Between Actors
Actors communicate through messages. Those messages are immutable POCO classes. If you use the proxy we mentioned earlier, you have two forms of communication available to you. The first is a “Request-Response with Ask method,” and the second is “Fire-and-forget with Tell method.”
Generally speaking, you should always use the Tell method. If you use Ask, you’d have to wait for the response, as this goes against the essential nature of Akka (and the very concept of having an asynchronous actor system).
Supervision Within the Actor system
Sometimes, actors fail. What happens then, and what should you do?
Each actor is a supervisor of all its children. As a result, any errors raised by the child will be passed to the parent.
Here’s an example. In the picture on the left, actor A is the parent of actors B, C, and D. Every problem raised by the child actors will be passed to the parent actor.
In this example, an error raised by actor C passes to actor A, and actor A then decides how to handle that error.
Akka.NET provides four different strategies for handling errors:
- Resume operating the child actor, keeping its accumulated internal state.
- Restart the child actor, clearing out its accumulated internal state.
- Stop the child actor permanently.
- Escalate the error to the next parent in the hierarchy, causing it to fail.
Escalation to the next parent actor should be avoided where possible. Instead, try to resolve the problem as close to the source as possible. To avoid escalation when working with Akka.NET, you will need to override the supervision strategy methods within actors. Simply put, Akka provides the base classes and so, to change the strategy, you have to override that method.
Actor Model Extensions for Akka.NET
Akka.NET is distributed as a set of NuGet packages.
Akka.Remote is a NuGet package that allows you to distribute the actors within your system across different machines. What’s nice about this package is that, from a developer’s perspective, it’s 100% transparent. Every actor has a unique address with its own transport such as a TCP host and port. Remote actors have addresses that look similar to HTTP addresses.
Akka.Cluster is a remote Akka extension that allows for elastic scaling.
Akka.Routing is another useful package. It allows you to create actors that can route messages sent to them to other actors. Take a look at the illustration below. In it, the actor is basically a router and can be treated as a single actor. Yet, there are multiple actors at play, with the router distributing messages among them.
There are various ways to distribute a message. For instance, you can implement Round Robin and have the messages handled in a circular order without priorities. The messages will be distributed to the three actors accordingly.
An Example of an Actor Model System
Let’s assume you’ve been tasked to build a smart home system with different types of sensors (i.e., gas, motion, or light sensors), as shown in the picture below.
We’ll call the parent actor the “house” actor.
One way to build the system would be to assign an actor to each sensor. An actor will read the value from its corresponding physical sensor every time you command it to do so. The actor can be treated as a dummy that handles only one message. You could call it ReadSensorValue, and it will send the results to the parent, a.k.a, the house actor.
The house actor should enable the registration of new sensors in the house and handle the results collected from the sensors. It should also have a scheduler that periodically sends the message ReadSensorValue to sensor actors. The scheduler should send the message at different intervals to each type of sensor, as some may need to be checked less frequently than others.
Each house actor will also require a child actor called CheckSensorValue. The house actor will forward messages collected from the sensors to CheckSensorValue to check if the values are above or below the desired limit.
Here’s what the hierarchy of this model would look like with multiple houses (but keep in mind that it has been simplified for the purposes of this article):
Advantages and Disadvantages of the Actor Model System
One of the main advantages of the actor model is that it allows you to create distributed concurrent systems without resorting to the typical .NET libraries and mechanics like threads, tasks, mutexes, etc.
This model’s locking capabilities can also be very useful, as we saw with the reservation actor code above. That allows us to handle situations in which two people try to change the state of an actor at the same time, and we did it without having to use a locking mechanism.
As far as testing goes, it can be more complicated compared to traditional systems. Even though Akka provides a test framework, it may still be harder to test actor systems because you usually wouldn’t write unit tests for actors (the exercises are generally quite simple and only define how to react to messages). So, to test the actors, you would need to create a system and start sending messages.
The learning curve required to create high-quality, robust actor model systems can be a disadvantage as well. Especially when you are first starting out, it may be hard to imagine all potential business requirements and come up with a way to translate them into an actor system. The setup itself is fairly straightforward, but creating the perfect internal logic may take some time.
However, once you get the hang of the setup and system design, the actor model can be very simple to explain to non-developers, from the flow of the messages to the behavior of the system. That can help achieve greater integration and transparency across different departments within your organization.
For best results, our advice would be to build actor systems in which every hierarchy or service is a bounded context, with the bounded contexts communicating with each other using a queue mechanism. That will allow you to create systems dedicated to particular parts of your business, where different bounded contexts handle specific processes.