1. Overview
Actors in Scala that use the Akka framework can help us to write distributed and concurrent programs on the JVM. These actors are lightweight, with over 2 million actors taking just about 1GB of heap. They are objects that can be created, restarted, or stopped.
2. Actors
With the introduction of Akka Typed, there has been a slight change in how actors are created. With Classic actors, we just had to extend the Actor trait, but with Akka Typed, we instead define typed behaviors that control how the actor responds to incoming messages.
Let’s see an example of how we create a typed actor:
object StringActor {
def apply() : Behavior[String] = Behaviors.receive {
case (context, message) =>
println(s"received message $message")
Behaviors.same
}
}
We just defined an actor with a Behavior[String], which implies that this actor can only receive messages of type String. Also available is the ActorContext, similar to classic actors for spawning new actors or gaining access to the ActorSystem.
We also defined the behavior after each message to remain the same.
We can create send a message to our actor as well:
val stringActor = ActorSystem(StringActor(),"StringActor")
stringActor ! "Hello World"
The String Actor we just created only accepts Strings. Any message of another type will result in a compiler error.
3. Actor Lifecycle
Actors can be started, restarted, and stopped. It’s important to understand when and how this happens and how we could take advantage of our programs’ lifecycles. For example, before an actor starts, we could attempt to connect to a database or log some information. We could also release resources that the actor used when we know the actor is about to be stopped.
We could also restore an actor to its previous state in the case of a restart.
3.1. PreStart
We can control what the actor does just before it starts. In classic actors, there was the preStart callback that’s only called once directly during the initialization of the first instance of the actor — that is, at the creation of its ActorRef.
But in Akka Typed, we use the Behavior.setup method instead to define what the actor should do at initialization before receiving any message:
object StringActor {
def apply() : Behavior[String] = Behaviors.setup { context =>
println("Before receiving messages")
Behaviors.receiveMessage[String] {
message =>
println(s"received message $message")
Behaviors.same
}
}
}
val stringActor = ActorSystem(StringActor(),"StringActor")
stringActor ! "Hello World"
By calling Behavior.setup, we gain control of the actor’s behavior before it starts. Each actor instance will run the code defined in the setup method before it starts reacting to incoming messages.
If we run the code, we see that the code just before Behaviors.receiveMessage in the Behavior.setup block is called before messages are processed.
3.2. PostStop
We also have control over what an actor should do when it’s about to be stopped. We could release resources used by the actor or close database connections.
In classic actors, the postStop hook was used for things like deregistering the actor from other services. This hook was guaranteed to run after message queuing had been disabled for the actor, and messages sent to a stopped actor were redirected to the deadLetters of the ActorSystem.
But in Akka Typed, we instead need to register to receive the PostStop signal, which is triggered when the actor is about to be stopped, by calling the receiveSignal method on the behavior of the actor:
object StringActor {
def apply() : Behavior[String] = Behaviors.setup { context =>
println("Before receiving messages")
Behaviors.receiveMessage[String] {
case "stop" =>
Behaviors.stopped
case message =>
println(s"received message $message")
Behaviors.same
}.receiveSignal {
case(_, PostStop) =>
println(s"stopping actor")
Behaviors.stopped
}
}
}
val stringActor = ActorSystem(StringActor(),"StringActor")
stringActor ! "stop"
In this example, we configured our actor to stop when it receives the message “stop”. If we run this code, we see that what’s defined in the PostStop partial fraction is called as the actor is stopped.
By registering to receive this signal, we can do what we’ll typically do when actors are stopped.
3.3. PreRestart
We also have control over controlling the actor when it restarts. When restarting an actor, common things include replaying events if it’s a stateful actor or reconnecting to external data sources. Similar to postStop, we can do this by registering to receive the PreRestart signal:
object StringActor {
def apply() : Behavior[String] = Behaviors.setup { context =>
println("Before receiving messages")
Behaviors.receiveMessage[String] {
case "stop" =>
Behaviors.stopped
case "restart" =>
throw new IllegalStateException("restart actor")
case message =>
println(s"received message $message")
Behaviors.same
}.receiveSignal {
case(_, PostStop) =>
println(s"stopping actor")
Behaviors.stopped
case (_, PreRestart) =>
println("Restarting Actor")
Behaviors.stopped
}
}
}
val stringBehaviour: Behavior[String] = Behaviors.supervise(StringActor()).onFailure[IllegalStateException](SupervisorStrategy.restart)
val stringActor = ActorSystem(stringBehaviour,"StringActor")
stringActor ! "restart"
We’ve configured our actor to throw an exception on the message “restart.” Now, classic actors will be automatically restarted whenever they encounter exceptions, but in the case of Akka Typed, the actor is stopped when these exceptions are encountered. Furthermore, to be able to have the actor restart, we have to define a Supervisor strategy on the behavior to define the kinds of exceptions or failures we want the actor to restart on:
val stringBehaviour: Behavior[String] = Behaviors.supervise(StringActor()).onFailure[IllegalStateException](SupervisorStrategy.restart)
val stringActor = ActorSystem(stringBehaviour,"StringActor")
stringActor ! "Hello World"
stringActor ! "restart"
By using a supervisor strategy to define the actor’s behavior, we’re adding a failure strategy to the actor’s functionality to do whatever we decide – in this case, to restart – if it encounters a specified failure.
When certain errors are encountered, we don’t want our actor to restart, but we do want it to be stopped. In this case, we could easily use a SupervisorStrategy.stop.
4. Conclusion
In this article, we’ve seen actors’ lifecycles and how we can gain control of actors in these lifecycles. When using actors, understanding these lifecycles can help write efficient and safe code, a powerful tool to have when building concurrent and distributed programs.
As usual, the source code can be found over on GitHub.