Scala/Akka Typed problem: You want to see the proper way to shut down Akka actors.
Solution
There are two main ways to shut down Akka Typed actors:
-
Send them a “graceful shutdown” message, and let them shut themselves down by calling
Behaviors.stopped
-
As a parent, stop a child actor with
actorContext.stop(actorRef)
Actors that are being stopped can also handle a PostStopped
signal they receive when they are being shut down by the Akka system.
These behaviors are shown in the following code. First, create a little Utils
object that we can use as a timer:
object Utils {
var startTime = 0L
def time = System.currentTimeMillis - startTime
}
Child messages
In the code that follows I’ll create an OOP-style child actor named OopChild
, and an FP-style child actor named FpChild
, so the next thing I do is create some messages outside of their objects that both actors can use:
// messages that both children can use
sealed trait MessageToChild
final case object GracefulShutdown extends MessageToChild
final case object HelloOopChild extends MessageToChild
An OOP-style actor
Next, create an OOP-style actor named OopChild
. I introduced this style in [oop-actor-intro], and I’ve added comments to the code to explain how it works:
object OopChild {
// the factory/constructor method
def apply(): Behavior[MessageToChild] =
Behaviors.setup(context => new OopChild(context))
}
private class OopChild(context: ActorContext[MessageToChild])
extends AbstractBehavior[MessageToChild](context) {
println(s"${Utils.time}: OopChild is started.")
// onMessage lets us handle normal messages
override def onMessage(message: MessageToChild): Behavior[MessageToChild] = {
message match {
case HelloOopChild =>
println(s"${Utils.time}: Hi, I’m OopChild.")
Behaviors.same
}
}
// onSignal lets us handle “signals” from the ActorSystem. I’m only
// interested in PostStop, but there a few other signals can be handled.
override def onSignal: PartialFunction[Signal, Behavior[MessageToChild]] = {
case PostStop =>
println(s"${Utils.time}: OopChild: Got the PostStop signal.")
// can’t use Behaviors.stopped here because you’re already stopped
println(s"${Utils.time}: OopChild could have done some cleanup here")
this
}
}
The reason I create an OOP-style actor first is because — at least for me — I find the code easier to read. It’s very clear that onMessage
and onSignal
are methods inside an actor class.
This is the first time I’ve showed the onSignal
method, and it lets you handle lifecycle “signals” that are sent to an actor by the Akka system. There are other signals an actor can receive, but for the purposes of showing how actors are shut down, PostStop
is the only signal I’m interested in.
Note: If you’ve written code for Android, Flutter, Drupal, or other systems, this is just like handling lifecycle events with those tools.
An FP-style actor
Next, here’s the source code for an FP-style actor. It’s similar to the OOP-style actor in that it also handles the PostStop
message, but its syntax for doing so is different. Also, this actor handles a GracefulShutdown
message to shut itself down. I’ve added comments to it to explain its behavior:
import Utils.time
object FpChild {
// the usual factory method
def apply(): Behavior[MessageToChild] = {
// i added output so you can see when `apply` is called
println(s"$time: FpChild: `apply` is entered.")
Behaviors.receive[MessageToChild] { (context, message) =>
message match {
// in the Guardian that follows, you’ll see that this actor is
// shut down with the use of this message
case GracefulShutdown =>
println(s"$time: FpChild: Initiating graceful shutdown.")
Behaviors.stopped { () =>
println(s"$time: FpChild: could have done some cleanup here")
}
}
}.receiveSignal {
// `receiveSignal` is the equivalent of the OopChild’s `onSignal`
// method. handle the PostStop signal here.
case (context, PostStop) =>
println(s"${Utils.time}: FpChild: Got the PostStop signal.")
Behaviors.same
}
}
}
The Guardian actor
Next, here’s the code for a “Guardian” actor. It will be created by my App
, and it’s responsible for (a) creating the two child actors, and then (b) shutting down those actors. Again I’ve added comments and println
statements to explain what’s happening:
import Utils.time
object Guardian {
// the messages this actor handles
sealed trait MessageToGuardian
final case object StopChild1 extends MessageToGuardian
final case object StopChild2 extends MessageToGuardian
final case object ShutDownTheGuardian extends MessageToGuardian
// the usual factory method. this line of code is long,
// so it’s wrapped onto two lines here.
def apply(): Behavior[MessageToGuardian] =
Behaviors.setup { context: ActorContext[MessageToGuardian] =>
println(s"$time: Guardian: `apply` is entered.")
// create two children
val fpChild: ActorRef[MessageToChild] = context.spawn(
FpChild(), "FpChild"
)
val oopChild: ActorRef[MessageToChild] = context.spawn(
OopChild(), "OopChild"
)
// handle the messages we receive. the main purpose of this code is
// to show the two different ways to shut down child actors.
Behaviors.receiveMessage { message: MessageToGuardian =>
message match {
case StopChild1 =>
println(s"$time: Guardian:" +
"Sending GracefulShutdown message to fpChild.")
fpChild ! GracefulShutdown
Behaviors.same
case StopChild2 =>
println(s"$time: Guardian:" +
"Stopping oopChild with `context.stop`.")
context.stop(oopChild)
Behaviors.same
case ShutDownTheGuardian =>
println(s"$time: Guardian: Received a shutdown message.")
Behaviors.stopped
}
}
}
}
The Akka Typed App
With all of that code in place, here’s the source code for an App
that demonstrates how to shut down actors:
object GracefulShutdownApp extends App {
// start the timer
Utils.startTime = System.currentTimeMillis
println(s"${Utils.time}: The App is started.")
// start the ActorSystem, which also starts the FpChild and OopChild actors
val actorSystem: ActorSystem[Guardian.MessageToGuardian] = ActorSystem(
Guardian(),
"Guardian"
)
// after a brief delay, tell the Guardian to stop the children
Thread.sleep(100)
actorSystem ! Guardian.StopChild1
actorSystem ! Guardian.StopChild2
// after another delay, tell the Guardian to stop itself
Thread.sleep(100)
actorSystem ! Guardian.ShutDownTheGuardian
// shut down the ActorSystem
Thread.sleep(100)
actorSystem.terminate()
}
With some Akka logging information removed and a few blank lines added, the output of this App
looks like this:
0: The App is started. 336: Guardian: `apply` is entered. 337: FpChild: `apply` is entered. 340: OopChild is started. 433: Guardian: Sending GracefulShutdown message to fpChild. 434: Guardian: Stopping oopChild with `context.stop`. 434: FpChild: Initiating graceful shutdown. 438: FpChild: Got the PostStop signal. 438: FpChild could have done some cleanup here ... 438: OopChild: Got the PostStop signal. 438: OopChild could have done some cleanup here ... 535: Guardian: Received a shutdown message.
If you look back through the code for the three actors and the App
, hopefully that output will match what you expected to see.
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |
Discussion
Possibly the most important thing to see in these examples — besides the proper syntax for the two approaches — is that both the OopChild
and FpChild
receive the PostStop
signal. This means that you can clean up any open resources you have — database connections, HTTP connections, files, etc. — before your actor shuts down.
Because of the way the code is implemented, FpChild
can do any necessary cleanup work in two places, (a) when it receives the GracefulShutdown
message, and (b) in receiveSignal
:
case GracefulShutdown =>
Behaviors.stopped { () =>
// (a) can do cleanup work here
}
receiveSignal {
case (context, PostStop) =>
// (b) can do cleanup work here
Behaviors.same
}
Because I wrote OopChild
without handling a graceful shutdown message, the only place it can do its cleanup work is in the PostStop
case inside onSignal
:
override def onSignal: PartialFunction[Signal, Behavior[MessageToChild]] = {
case PostStop =>
// can do cleanup work here.
// can’t use Behaviors.stopped here because you’re already stopped.
this
}
See Also
The Akka Typed Lifecycle page discusses the lifecycle signals that actors can receive, include PostStop
, PreRestart
, and Terminated
.