Chapter 1. The Actor Model
It is essential to understand how actors were meant to be used to realize their full value, and thatâs what you will learn in this chapter. Here, we explore actorsâhow they work, and how they interact with one another and the outside world.
Many of the techniques we use to design software teach you that it is important to look at the real world before writing code. We must understand the use case for the software. We need to ask questions about who will be using it and how they will use it. These questions are critical to the design of good software. But in the pursuit of our design, we often forget to ask ourselves an important question: âHow long will it take?â Yet, anyone who has written a highly concurrent system knows that time is an integral part of how we develop our software.
Letâs explore a bit far afield from software for a momentâdonât worry, weâll show how this relates to software development in a moment!
Reality Is Eventually Consistent
Consider the example of reaching for a cup of coffee. On the surface, this might seem like a very simple example. You reach out and pick up a cup of coffee. Not much is happening there. But letâs look deeper.
To be able to pick up the coffee, you need to know where it is. You can look over and see the cup, but are you seeing it as it is, or as it was? The information you use to determine the location of the cup is based on the photons reflected from that cup. Those photons take time to travel. Further, when our eyes receive the data, they must perform some processing before sending the data to our brain. Further processing needs to be done by other areas of our nervous system in order to communicate the desire to move our arm. Finally, our arm must stretch out to take the cup.
At each stage in the process, small amounts of delay have leaked into the system. Picking up a coffee cup is a fairly simple operation, though, in a very static environment, so those delays donât have a large effect. But as the nature of the environment changes, as events begin to happen faster and more frequently, those small delays can add up. Picking up a cup of coffee is fairly trivial. Trying to catch that same cup of coffee if it falls off the table, without spilling it, is much more challenging. Trying to catch multiple cupsâ¦well, now weâve entered the realm of the impossible.
The truth is that our reality is bounded by the speed of light. The laws of physics, and the speed of light, put an upper bound on causality. Unless two things occupy the same space (which, of course, is impossible), for one event to have an effect on another, there must be a passage of time between the two. Two observers watching the same event from different distances will experience that event at different times. The person closer to the event will experience it first, while the person further away will experience it slightly later. Yet, despite the difference in time, both experiences are real. Both are equally valid. It is this idea, rooted in physics, on which the Actor Model is based.
The truth is that we live our lives operating on out-of-date information. On a small scale, our cells are communicating with one another through messages passed in the form of hormones. On a larger scale, we go through our daily lives talking to people, watching the news, reading the latest developments on a blog. All of these things are in one way or another just a form of asynchronous messaging. In fact, when we break it down, there is nothing in our lives that is being experienced synchronously.
Even our computers behave this way. Every operation a computer performs is done by sending a signal over some kind of medium, whether itâs electrical signals, photons, or something else.
So, if everything in the world is being experienced in an asynchronous manner, why do we put so much effort into trying to write software that is synchronous? We teach ourselves that we should model software on the real world, yet we ignore the fundamental concept of time. Wouldnât it be better if we modeled it on the real world and built our software using asynchronous events or messages?
As a fun exercise, letâs reverse this idea. Letâs take the real world and model it through the lens of traditional synchronous software. How does this look? What are the consequences?
Letâs go back to our coffee example. When your brain decides that you want a sip of coffee, it must first pause time, at least in a localized fashion. You need to halt the world around you so that you can ensure that nothing changes between the moment you decide to have that coffee and the moment when the coffee reaches your lips. Nothing can interfere with that. If you reach out to take the coffee, only to discover someone else got there first, you have chaos. Instead, you freeze everything around you, locking the state in place, so that you can guarantee that wonât happen. Anyone else who might have been reaching for that same cup will now be stopped, frozen until you have completed your action. This means not just pausing another person, but also pausing the air, the light, everything around that cup. Everything between the cup and you must stop, or else your state might become invalid. This sounds very complicated, much more complicated than if we had just taken the time to model the system correctly in the first place.
This is one of the fundamentals of the Actor Modelâwith it, we can build software that reflects how reality actually works instead of assuming a frozen-in-time world that doesnât actually exist.
Deconstructing the Actor Model
In 1973, Carl Hewitt, along with Peter Bishop and Richard Steiger, set out to help solve some of these problems in a paper called âUniversal Modular Actor Formalism for Artificial Intelligence.â Although many programming paradigms are based around mathematical models, the Actor Model was, according to Hewitt, inspired by physics. It has evolved over the years, but the fundamental concepts have stayed the same.
It is important to understand that when working with Akka, you can write code without using the Actor Model. The fact that you are using actors does not imply that you are using the Actor Model. There are many developers who have been using Akka for years and are unaware of the Actor Model.
The difference between using the Actor Model and simply using actors is the way that you treat those actors. If you use those actors as the top-level building blocks, such that all code in your system resides within a system of actors, you are using the Actor Model. On the other hand, if you build your system such that you have actors residing within nonactors, then you are not using the Actor Model.
It is also important to understand that programming in the Actor Model is not about any one tool or technology. Entire languages have been written around the idea of the Actor Model. In this way, it is more like a programming paradigm than a set of tools. Learning it is like learning object-oriented programming (OOP) or functional programming. And like those two methodologies, the Actor Model predates Akka, and there are many other implementations of it.
The Pony language, for instance, is based on actors and can be easily used to implement the Actor Model. Erlangâs processes are equivalent to Akka actors; they are a fundamental feature of Erlang. In Ada, the idea of the Task exists, along with messages called âentriesââwhich are queued by the asynchronous Tasksâmeaning the Actor Model can be implemented in this language, as well.
To implement the Actor Model, there are a few fundamental rules to follow:
-
All computation is performed within an actor
-
Actors can communicate only through messages
-
In response to a message, an actor can:
-
Change its state or behavior
-
Send messages to other actors
-
Create a finite number of child actors
-
Of course, if you are familiar with Akka, you can immediately recognize how these components have been implemented, but letâs talk about them outside of that context for a moment so that we can better understand how the basic Actor Model might differ from the Akka implementation.
All Computation Is Performed Within an Actor
Carl Hewitt called the actor the fundamental unit of computation.1 What does it mean to be the fundamental unit of computation? It means that when you build a system using the Actor Model, everything is an actor. Whether you are computing a Fibonacci sequence or maintaining the state of a user in your system, you do so within an actor, or multiple actors.
This idea that everything is an actor is not without difficulties, though. If every computation needs to happen within an actor, this implies that every function and every state variable could be its own actor. And even though this is technically possible, itâs not always pragmatic. Often, we have groups of related functions and it is usually more convenient to wrap all those functions within a single actor. Doing so doesnât violate the Actor Model. But how do we decide where to draw the line? We will discuss this in more detail in Chapter 3 when we learn about domain-driven design (DDD). The building blocks we introduce for DDD are excellent candidates to turn into actors and we can use them as guiding principles for when to create a new actor.
Actors in the Actor Model embody not only state but also behavior. This might sound suspiciously like the definition of OOPâin fact the two are very closely related. Alan Kay, who coined the term âobject-oriented programmingâ and was one of the original creators of the Smalltalk language, was largely influenced by the Actor Model. And while Smalltalk and OOP eventually evolved away from their Actor Model roots, many of the basic principles of the Actor Model remain in our modern interpretation of OOP. In truth, the original focus of OOP was not the objects themselves but rather the messages flowing between them.
Note
OOP models the fundamental unit of computation as an object (an instance of a class), and is familiar to developers coming from Java and similar languages. Functional programming, on the other hand, models computation around functions and their applications, and is seen in languages such as Lisp and Haskell.
Another aspect of the model that is useful for highly concurrent applications is the idea that an actorâs state is isolated. It is never directly exposed to the outside world. We donât allow actors to look at or modify the state of another actor, except indirectly through messages. This isolation applies equally to the actorâs behavior. The internal methods or mechanisms of the actor are never exposed to other actors. In fact, state and behavior can largely be treated as the same thing within an actor (more on this later).
Actors in the model can take many different forms. They can exist as highly technical constructs like a database access layer, or they can be domain-specific constructs like a person or schedule. They can even be used to perform simple mathematical operations. Anything in the system that needs to perform any amount of computation is an actor.
The actor is the fundamental building block in the Actor Model as well as Akka-based applications, as we will see.
Actors Can Communicate Only Through Messages
We have created our actors in an isolated fashion so that they never expose their state or their behavior. This is an important element of the Actor Model. Because we have isolated them in this way, we need to find other ways by which we can communicate with them and learn about the state of the system.
Messages are the backbone of all communication in the Actor Model. They are what enables communication between actors.
Every actor, when created, is given an address. This address is the entry point for communication with that actor. You cannot use the address to access the actor directly, but you can use it to send messages to that actor.
Note
Akka separates the concept of an address from a reference. Most actor communication is done using references. Akka actors also have an address that can be used in some circumstances; however, they are generally avoided. Nonetheless, whether you are using a reference or an address, the basic principle is the same: you have a means to locate the mailbox for the actor so that you can deliver a message to it.
Messages that are sent to an actor are pieces of immutable data. These messages are sent to a mailbox that exists at the address provided for the destination actor. What happens after the message reaches the mailbox is beyond the control of the sender. Messages can be delivered out of order, or they might not even be delivered at all. The Actor Model provides an At Most Once guarantee of delivery. This means that failure is an option, and if we need to guarantee delivery, we will need other tools to enable that.
Note
At Most Once delivery is the default guarantee provided by the Actor Model and by Akka. However, At Least Once delivery can be built on top of At Most Once delivery, and there are mechanisms within Akka to provide that.
Akka further provides a slightly stronger guarantee of ordering. In Akka, message order is guaranteed between any pair of actors. That is, if one actor sends multiple messages to another, we can guarantee that the order of those messages will be preserved.
After the messages are in the mailbox, the actor can receive those messages. However, messages are received one at a time. No single actor can process two messages simultaneously. This is a critical guarantee because it allows us to operate within the actor in a single-threaded illusion. Actors are free to modify their internal state with no worries about whether another thread might also be operating on that state. As long as we maintain the single-threaded illusion, our state is protected against any of the normal concerns of concurrency.
The exact nature of the messages is dependent on the actorâs functionality. For domain actors, it would be common to see the messages as commands or events presented by using domain terms like âAddUserâ or âCreateProjectâ in our scheduling example. Of course, if the actor is more technical in nature, the messages can be more technical, like âSaveâ or âCompute.â
Because actors communicate only through messages, it allows for some interesting options. As long as the actor on the other end of the mailbox is able to process the message, it can take any form necessary to get the job done (Figure 1-1). This means that the receiving actor can process the message directly or it can act as a proxy for the message, simply forwarding it on to another actor to do the necessary work. It might also break the message into smaller chunks, sending those chunks on to other actors (or possibly itself) to be processed. In this way, the details of how the message is processed can be as simple or as complex as required, and the details of this implementation are transparent to the sender of the message.
Actors Can Create Child Actors
In the Actor Model, everything is an actor, and actors can communicate only through messages. But actors also need to know that other actors exist.
One of the actions that is available to an actor when it receives a message is that it can create a finite number of child actors. The parent will then know about the child actor(s) and will have access to the childâs address. This means that a parent actor can send messages to a child actor.
In addition to knowing about actors by creating them as children, an actor can pass the address to another actor through a message. In this way, a parent could inform a child about any other actor the parent is aware of, including itself. Thus, a child actor can know the address of its parent or siblings with very little difficulty. With a little more work, the child can know about other actors that exist in the hierarchy, as well. In addition, if the address used for the actor follows a set pattern, it is possible to synthesize addresses for other actors, though this can create undesirable complexity, and possibly security concerns, if not used carefully.
This hierarchy of actors means that with the exception of the root, all actors will have a parent and any actor can have one or more children. The collection of these actors, starting from the root and working down through the tree, is called an actor system.
Each actor in the actor system can always be uniquely identified by its address. It is not critical that it follow any particular pattern, so long as the address can be guaranteed to uniquely identify an actor. Akka uses a hierarchical pattern of naming, much like a directory structure (similar to that shown in Figure 1-2), or you could use randomly generated unique keys.
In Figure 1-2, the root actor is the top-level actor under which all others will be created. The root actor then has two children A and B. The address for A is Root/A and the address for B is Root/B. Beneath A are two more actors named A and B. Even though these share the name of other actors, their address is still unique (Root/A/A and Root/A/B). Child B also has a child of its own with the path Root/B/A. Again, the address is unique, even if the name is not. This is just one example of how addresses can be created.
Child actors are one of the most valuable integration techniques in the Actor Model, and also form the basis of actor supervision in Akka, as we will see.
Actors Can Change Their State or Behavior
In addition to sending messages and creating child actors, actors can also change how they will react to the next message. We often use the term âbehaviorâ when talking about how an actor will change, but this is slightly misleading. The change in behavior, or the change in how an actor will react, can also manifest itself as a change of state.
Letâs consider the scheduling example discussed in the introduction. If we have an actor that represents a personâs availability, the initial state might indicate that this person is available for a project. When a message comes in to assign that person to a project, the actor can alter itself such that when a new message comes in, it will show as unavailable. Even though this seems to be a change in state, we still consider it to be a change to the actorâs behavior because the actor is now behaving like it has a value of unavailable when the next message comes in.
We can also have an actor that changes the computation that it will perform when it receives the next message. The actor representing a person in this system might be able to exist in different states. While that person is employed and available to be on a project, they can exist in an Active state. However, certain conditions can result in that person moving to an Inactive state (termination, extended leave, etc.). While in an Active state, project requests can be processed as normal. However, if a message comes into the actor that puts it into an Inactive state, the system might begin rejecting project requests. In this case, the actor alters the computation that it performs when the next message comes in.
Because actors can alter their behavior and state using this method, it makes them an excellent tool for modeling finite-state machines. Each behavior in the system can represent a state, and the actor can move from one state to the next when it receives a message.
Figure 1-3 shows a simple finite-state machine representing a person in the system. In this example, a person can exist in a limited set of states. He can be Created (but not yet Active), Active, Inactive, or Terminated. When a person is created in the system, he enters the Created state. In this state, he is not yet Active. This might occur when the person has been hired but has not yet begun employment, for example. After a person becomes Active, he canât go back to the Created state. An Active person can be scheduled to work on a project. At some point, that person might become Inactive, perhaps because he goes on leave. While Inactive, the person cannot accept any requests. He can move freely between the Active and Inactive states. Eventually, the person might leave the company, at which point the system moves the person to the Terminated state. After a person enters the Terminated state, he cannot go back to the Active or Inactive states. He can go from the Terminated state to Created to accommodate the possibility that the person is rehired later on.
Note
Actors are an excellent way to model a finite-state machine and are commonly used in this way. Akka provides specific tools that make the creation of finite-state machines easier in the form of the Akka FSM.
We obviously can create far more complex scenarios, but this gives you an idea of the types of things that you can accomplish by altering the behavior of an actor. Whether it is altering the messages that are accepted, altering the way those messages are handled, or altering the state of the actor, this all falls under the heading of behavior. Akka provides several sophisticated techniques for altering actor behavior, as we will discuss in detail.
Everything Is an Actor
Now that you understand the building blocks of an actor and of a system of actors, letâs return to the idea that everything is an actor and see what that means, and what it looks like.
Letâs consider our scheduling domain again. A person in our scheduling domain will have a variety of associated information. Each person might have a schedule indicating availability. Of course, that schedule would break down further into more discrete time periods.
In a more traditional object-oriented architecture, you might have a class to represent the person. That class would then have a schedule class associated with it. And that schedule might break down further into individual dates. When a request comes into the system, it would call functions on the person class, which would in turn call functions on the schedule class and the individual dates.
The Actor Model version of this isnât much different, except now you have an
actor representing the person. But in the Actor Model, the schedule can
also be an actor because it potentially makes a computation. Each
individual date can also be an actor because they might also need to make a
computation. In fact, it is possible that the request itself can have an
actor associated with it. In some cases, the request might need to
aggregate information as it is computed by the other pieces of the
model. In this case, we might want to create a RequestWorker
that will
handle the aggregation.
As Figure 1-4 shows, in this model, instead of calling functions that will alter the state of
the objects, you pass messages between them. Messages like
CheckAvailability
flow from the Person
actor into the ScheduleActor
actor. In this example, the Person
represents the topmost actor. The
Schedule
is a child of Person
, and the individual Date
s are children of
the Schedule
. And because we are using the Actor Model, all messages are
handled concurrently. This way, we can compute multiple dates in the
schedule at the same time; we donât need to wait for one to complete
before moving on to the next. In fact, we donât even need to complete the
previous request before starting on the next one. The schedule can be
working on two simultaneous requests, each looking at overlapping dates.
Due to the single-threaded illusion, if two requests try to schedule the
same date, only the first request will succeed. This happens because the
same actor is used to process both requests for that date, and the actor
can process only one message at a time. On the other hand, if there is
no overlap in dates, both requests can be processed and completed
simultaneously, allowing us to make better use of our resources.
Uses of the Actor Model
The Actor Model is a powerful tool, and when applied carefully, can be effective at providing highly scalable, highly concurrent applications. But like any tool, it is not perfect. If you apply it incorrectly, the Actor Model can be just as likely to create complex code that is difficult to follow and even more difficult to debug. After you learn the tools, you might decide that you want to apply the Actor Model at all levels of your application, creating a true Actor System. But early on, it is sometimes safer to just get your feet wet and ease into it. In later chapters, we will explore these ideas further and provide additional guidelines about what an individual actor should be modeling.
One thing to realize about the Actor Model is that, like other techniques, there are times when you might want to apply it, but there are other times when you might not. You might find that in certain cases an alternative model is more suitable for the task at hand. And in those cases, you should not be afraid to use those tools or techniques. The Actor Model is not an all-or-nothing proposition. The key is to figure out where you need it and where you donât, and then create a clear and distinct line separating the two.
Defining Clear Boundaries
Many successful systems are modeled with actors representing top-level entities in the system, and then using a more functional approach within those actors. There is nothing wrong with this approach. In addition, it is common to see a small group of actors wrapped within an a interface such that clients of that interface arenât aware that they are communicating with actors. Again, this is a good approach. Both create clear boundaries. In the first case, you have actors only at the top level. All of the implementation is done by using functional programming. In the second case, the interface creates a boundary between what are actors and what are not.
If you have decided to use the Actor Model for a portion of your application, you should stick to the model throughout that section. You should only break from the model on clearly defined boundaries. These boundaries could be the boundary between your frontend and backend services. Or, it could be the boundary between two different microservices in your application. There are many ways in which to partition your application and many ways to apply the Actor Model. You should, however, avoid breaking the Actor Model mid context. One of the things that contributes to messy, complex code is when the code transitions between styles with no clear pattern or explanation as to why the transition has occurred. You donât want to find yourself in a situation in which you jump into and out of the Actor Model multiple times within a single context. Instead, look to break from the model along consistency boundaries or domain boundaries.
Letâs consider a concrete example. In our scheduling domain, we might want to build a library to handle the scheduling. Within that library, we would prefer to use the Actor Model, but outside the boundaries of the library we want to use other techniques. This is a good example of a system boundary where it is OK to transition from one model of computation to another.
One way to handle this would be to expose everything as actors. The external client code would be fully aware that it is interfacing with actors. Thus, we might have a message protocol that looks something like this:
object
ProjectProtocol
{
case
class
ScheduleProject
(
project
:
Project
)
case
class
ProjectScheduled
(
project
:
Project
)
}
Using this code within the client would mean sending a message along the lines of this:
def
scheduleProject
(
details
:
ProjectDetails
)
:
Future
[
Project
]
=
{
val
project
=
createProject
(
details
)
val
result
=
(
projects
?
ProjectProtocol
.
ScheduleProject
(
project
))
.
mapTo
[
ProjectProtocol.ProjectScheduled
]
.
map
(
_
.
project
)
result
}
This is OK, but itâs not great. The problem here is that we have actor code interspersed with other code. This other code might have different concurrency mechanisms. There can actually be several steps in this process. You might first need to create a project, and then schedule the project and potentially perform other operations, as well. Each of these can involve going to the scheduling system, which means interfacing with actors. Yet there might be other parts of the code that communicate with other contexts that donât use actors, so there can be function calls and futures and other mechanisms here, as well. This constant jumping in and out of the Actor Model can become difficult to follow when itâs not well isolated. A better approach in this case is to create a wrapper API over the top of your actors, as demonstrated here:
class
ProjectApi
(
projects
:
ActorRef
)
{
def
scheduleProject
(
project
:
Project
)
:
Future
[
Project
]
=
{
val
result
=
(
projects
?
ProjectProtocol
.
ScheduleProject
(
project
))
.
mapTo
[
ProjectProtocol.ProjectScheduled
]
.
map
(
_
.
project
)
}
}
This thin wrapper around the actors provides the insulation layer. It gives us a clear transitional boundary between what are actors and what are not. Now, when we want to use this API, it looks like the following:
def
scheduleProject
(
details
:
ProjectDetails
)
:
Future
[
Project
]
=
{
val
project
=
createProject
(
details
)
val
result
=
projectApi
.
scheduleProject
(
project
)
result
}
The code is shorter, but really we have just moved any complexity within a separate function. So is that really better? The advantage of this approach is that the client code doesnât need to be aware that actors are being used internally. Actors in this case have become an implementation detail rather than an intrinsic part of the API. Now when we use this API, we are taking advantage of it using function calls, which means when we nest them alongside other function calls, everything looks more consistent and more clean. As the code base grows, and the actors become more complex, this kind of isolation can be critical to keeping the system maintainable.
Should we do this everywhere? Should all actors have an API wrapper around them so that the application never needs to know that it is dealing with actors? The simple answer is âno.â Within our scheduling library, which is built using the Actor Model, this kind of wrapper interface is not only unnecessary, it creates unwanted complexity. It actually makes it more difficult to work with the actors within the actor system. When weâre using the Actor Model we should be dealing with actors, and we should not be trying to hide that fact. Itâs only on the boundaries of the systemâwhere we want to transition to a different modelâthat we should be creating this kind of isolation.
When Is the Actor Model Appropriate?
We have decided that there are times to use actors by themselves and other times for which we might use the full Actor Model. And we know that when we use them, we need to be careful to do so within a clearly defined context. But when is the right time to use them? When should we use standalone actors and when should we build a full-fledged actor system? Of course, it is impossible to say you should always use one or the other in a certain situation. Each situation is unique. Still, there are some guidelines that you might take into account when making the decision.
The first obvious candidate for using actors is when you have a high degree of concurrency and you need to maintain some sort of state. Maintaining concurrent state is the bread and butter of the Actor Model.
Another obvious case for actors and the Actor Model is when youâre modeling finite-state machines, as an actor is a very natural construct for this task. Again, if you are limited to only a single finite-state machine, a single actor might do the job, but if you are going to have multiple machines, possibly interacting with one another, the Actor Model should be considered.
A more subtle case that might suit actors is when you need a high degree of concurrency, but you need to be careful in how that concurrency is managed. For example, you might need to ensure that a specific set of operations can run concurrently with other operations within your system, but also that they cannot operate concurrently with one another. In our scheduling example, this might mean that you want multiple users to be able to concurrently modify projects in the system, but you donât want those users to be able to modify the same project at the same time. This is an opportunity to use the single-threaded illusion provided by actors to your advantage. The users can all operate independently, but you can funnel any changes to a particular project through an actor associated with that project.
Conclusion
As you can see, the Actor Model is indeed a powerful tool, and like all powerful tools, you need to understand and carefully control it to gain the most advantage from it. You should not blindly run into every problem assuming that the Actor Model will always be the best fit. Still, after you have a solid understanding of itâand how and when to apply itâyou will find that it can in fact be applied to a wide variety of cases. Because it naturally reflects the asynchronous nature of the real world, it is an excellent tool for modeling a broad range of circumstances. After you master it, you can shake loose the shackles of global consistency and begin to understand that nothing happens instantly and everything is relative to the observer.
The Actor Model is a different paradigm for organizing your application functionality, with many advantages. The foundation we laid in this chapter will be extended in the remainder of this book to show you how to build good actors and take full advantage of the power of the model.
As you continue, however, keep in mind that many elements of the Actor Model will seem unusual and restrictive at first. There are good reasons for such restrictions, and the power of the model more than makes up for the initial effort to fully master it.
1 For more on this, see the video âHewitt, Meijer and Szyperski: The Actor Model (everything you wanted to knowâ¦)â.
Get Applied Akka Patterns now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.