I am brand new to Akka (Java lib, v2.3.9). I am trying to follow the supervisor hierarchy best practices, but since this is my first Akka app, am hitting a mental barrier somewhere.
In my first ever Akka app (really a library intended for reuse across multiple apps), input from the outside world manifests itself as a Process
message that is passed to an actor. Developers using my app will provide a text-based config file that, ultimately, configures which actors get sent Process
instances, and which do not. In other words, say these are my actor classes:
// Groovy pseudo-code
class Process {
private final Input input
Process(Input input) {
super()
this.input = deepClone(input)
}
Input getInput() {
deepClone(this.input)
}
}
class StormTrooper extends UntypedActor {
@Override
void onReceive(Object message) {
if(message instanceof Process) {
// Process the message like a Storm Trooper would.
}
}
}
class DarthVader extends UntypedActor {
@Override
void onReceive(Object message) {
if(message instanceof Process) {
// Process the message like Darth Vader would.
}
}
}
class Emperor extends UntypedActor {
@Override
void onReceive(Object message) {
if(message instanceof Process) {
// Process the message like the Emperor would.
}
}
}
// myapp-config.json -> where the actors are configured, along with other
// app-specific configs
{
"fizzbuzz": "true",
"isYosemite": "false",
"borderColor": "red",
"processors": [
"StormTrooper",
"Emperor"
]
}
As you can see in the config file, only StormTrooper
and Emperor
were selected to receive Process
messages. This ultimately results with zero (0) DarthVader
actors being created. It is also my intention that this would result with a Set<ActorRef>
being made available to the application that is populated with StormTrooper
and Emperor
like so:
class SomeApp {
SomeAppConfig config
static void main(String[] args) {
String configFileUrl = args[0] // Nevermind this horrible code
// Pretend here that configFileUrl is a valid path to
// myapp-config.json.
SomeApp app = new SomeApp(configFileUrl)
app.run()
}
SomeApp(String url) {
super()
config = new SomeAppConfig(url)
}
void run() {
// Since the config file only specifies StormTrooper and
// Emperor as viable processors, the set only contains instances of
// these ActorRef types.
Set<ActorRef> processors = config.loadProcessors()
ActorSystem actorSystem = config.getActorSystem()
while(true) {
Input input = scanForInput()
Process process = new Process(input)
// Notify each config-driven processor about the
// new input we've received that they need to process.
processors.each {
it.tell(process, Props.self()) // This isn't correct btw
}
}
}
}
So, as you can (hopefully) see, we have all these actors (in reality, many dozens of UntypedActor
impls) that handle Process
messages (which, in turn, capture Input
from some source). As to which Actors are even alive/online to handle these Process
messages are entirely configuration-driven. Finally, every time the app receives an Input
, it is injected into a Process
message, and that Process
message is sent to all configured/living actors.
With this as the given backstory/setup, I am unable to identify what the "actor/supervisor hierarchy" needs to be. It seems like in my use case, all actors are truly equals, with no supervisory structure between them. StormTrooper
simply receives a Process
message if that type of actor was configured to exist. Same for the other actor subclasses.
Am I completely missing something here? How do I define a supervisory hierarchy (for fault tolerance purposes) if all actors are equal and the hierarchy is intrinsically "flat"/horizontal?
If you want to instantiate no more than one instance for every your actor - you may want to have SenatorPalpatine
to supervise those three. If you may have let's say more than one StormTrooper
- you may want to have JangoFett
actor responsible for creating (and maybe killing) them, some router is also good option (it will supervise them automatically). This will also give you an ability to restart all troopers if one fails (OneForAllStrategy
), ability to broadcast, hold some common statistic etc.
Example (pseudo-Scala) with routers:
//application.conf
akka.actor.deployment {
/palpatine/vader {
router = broadcast-pool
nr-of-instances = 1
}
/palpatine/troopers {
router = broadcast-pool
nr-of-instances = 10
}
}
class Palpatine extends Actor {
import context._
val troopers = actorOf(FromConfig.props(Props[Trooper],
"troopers").withSupervisorStrategy(strategy) //`strategy` is strategy for troopers
val vader = actorOf(FromConfig.props(Props[Vader]), "vader")
override val supervisorStrategy = OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1) //stategy for Palpatine's children (routers itself)
val strategy = OneForOneStrategy(maxNrOfRetries = 100, withinTimeRange = 1) //stategy for troopers
def receive = {
case p@Process => troopers ! p; vader ! p
case t@Terminted => println(t)
}
}
That creates broadcast pools based on standard akka-config. I also shown that you can customize supervision strategies for them separately.
If you want some of actors to ignore message by some reason - just implement this logic inside actor, like:
class Vader extends Actor {
def receive {
case p@Process => ...
case Ignore => context.become(ignore) //changes message handler to `ignore`
}
def ignore = {
case x => println("Ignored message " + x)
case UnIgnore => context.become(process)//changes message handler back
}
}
This will configure ignore/unignore dynamically (otherwise it's just a simple if
). You may send Ignore
message to actors based on some config:
val listOfIgnorantPathes = readFromSomeConfig()
context.actorSelection(listOfIgnoredPathes) ! Ignore
You can also create broadcaster for palpatine in the same way as trooper's router (just use groups instead of pools), if you want to control heterogenous broadcast from config:
akka.actor.deployment {
... //vader, troopers configuration
/palpatine/broadcaster {
router = broadcast-group
routees.paths = ["/palpatine/vader", "/palpatine/troopers"]
}
}
class Palpatine extends Actor {
... //vader, troopers definitions
val broadcaster = actorOf(FromConfig.props(), "broadcaster")
def receive = {
case p@Process => broadcaster ! p
}
}
Just exclude vader from routees.paths
to make him not receiving Process
messages.
P.S. Actors are never alone - there is always Guardian Actor (see The Top-Level Supervisors), which will shut down the whole system in case of exception. So eitherway SenatorPalpatine
may really become your rescue.
P.S.2 context.actorSelection("palpatine/*")
actually alows you to send message to all children (as an alternative to broadcast pools and groups), so you don't need to have a set of them inside.