javaakkaactorfault-toleranceakka-supervision

How to determine Akka actor/supervisor hierarchy?


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?


Solution

  • 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.