11  Aktor-Modell und Umsetzung im Akka-Framework

11.1 Aktor-Modell

  • Idee aus den 70er Jahren (Hewitt, Bishop und Steiger (1973))
    • lange exotisch (u.a. Erlang)
    • hauptsächlich in Kommunikationssyst.: Telefonvermittlung, Whatsapp, RabbitMQ
    • inzwischen “Hot Topic” (u.a. Swift)
  • im Prinzip nebenläufige Objekte
  • eigener Zustand, eigenes Verhalten
  • Kommunikation mit anderen Aktoren durch Senden von Nachrichten (Methodenaufruf)
  • Nachrichtenzustellung durch Ablaufumgebung sichergestellt

eigene Darstellung

eigene Darstellung

11.1.1 Elemente des Aktor-Modells

  • kein geteilter Zustand zwischen Aktoren
    • keine Synchronisierung erforderlich
  • jeder Aktor hat eigene Mailbox
    • eingehende Nachrichten
    • Nachrichten werden sequentiell bearbeitet
  • Nachrichten werden asynchron zugestellt
  • ein Aktor kann neue Aktoren erzeugen
    • Verteilung von Teilaufgaben

Apache Software Foundation (2021)

Apache Software Foundation (2021)
  • Aktor kann Nachricht nehmen und verarbeiten (verbrauchen)
  • oder unverändert weiterleiten
  • Alternativ: Verarbeitung = neue Nachricht(en) erzeugen und weiterleiten
  • Aktor kann Nachrichten an sich selber senden

11.1.2 Nachrichten

  • Aktor interagieren durch Nachrichtenaustausch
  • Nachrichten werden asynchron zugestellt
    • empfangender Aktor blockiert nicht bis zum Empfang
  • Mailbox \(\neq\) Channel
    • mehrere Produzenten für eine Mailbox möglich
    • ein Empfänger behandelt die Nachrichten mehrere anderer Aktoren, die in seiner Mailbox ankommen, synchron
    • “put” und “take” sind atomare Operationen
  • Nachrichten immutable
    • Aktor kann Nachricht nehmen und verarbeiten (verbrauchen)
    • oder unverändert weiterleiten
    • Alternativ: Verarbeitung = neue Nachricht(en) erzeugen und weiterleiten
  • Aktor kann Nachrichten an sich selber senden

11.1.3 Aktoren \(\neq\) Threads

  • Die Anzahl der Aktoren kann größer sein, als die Zahl der sinnvoll zu behandelnden Threads (wie bei Fibers, Go-Coroutinen etc.)
  • Es gibt eine Komponente im Aktor-System (“Dispatcher”), die eine Zuordnung von Aktor zu Thread (z.B. aus einem Threadpool) übernimmt.

eigene Darstellung

eigene Darstellung
  • Die Verarbeitung einer Nachricht aus der Mailbox eines Aktors wird vom Dispatcher ausgewählt und in einem Thread ausgeführt
  • Der commonPool bei Java ist vom Typ ForkJoinPool und hat eine feste Thread-Anzahl wie ein Fixed Thread Pool, unterstützt aber zusätzlich auch “Work-Stealing”.

11.2 Aktoren überwachen Aktoren

11.2.1 Aktor-Programme und Fehler

Aktoren überwachen andere Aktoren (solche “Überwacher” heißen supersivor).

Statt “defensive programming” oder Exception Handling: “let it crash”:

  • Worker (Aktor, der konkrete Anwendungsaufgabe umsetzt) ist nur für Verarbeitung korrekter Eingabeparameter gemacht.
  • Fehlerbehandlung wird in den Supervisor verschoben.

11.2.1.1 Vorteile

  • Programme sind einfacher (Fehlerbehandlung ausgelagert).
  • Da Aktoren getrennt sind und keine Variablen teilen, besteht keine Gefahr, dass mehr als ein Programmteil abstürzt (Worker stürzt ab, Supervisor bleibt).
  • Supervisor kann weitergehende Fehlerbehandlungsstrategien auch für unvorhergesehene Situationen implementieren und zentrale Protokollierung vornehmen.

11.2.2 Hierarchie von Error Kernels

Aktoren (Supervisor) überwachen Aktoren (Worker oder Supervisor)

  • sie werden über Abstürze informiert
  • Ersatz für Exception Handling
  • sehr robust

Butcher (2014, 135)

Butcher (2014, 135)

“Increasing Risk”: Aktor mit höherem Risiko sollen weiter unten in der Hierarchie angeordnet werden.

11.2.3 Error Kernel Design Pattern

  • Der Teil eines Programms der absolut korrekt sein muss, da er andere (potenziell unsicherere) Teile überwacht und ggf. neu startet.
  • Man sollte bestrebt sein, den Error Kernel zu minimieren, damit wenigstens dort Fehlerfreiheit garantiert werden kann.
  • Im Aktor-Paradigma sollte jeder Prozess der einen Anwendungszweck hat (Worker) einen übergeordneten Supervisor-Prozess haben, der als dessen Error-Kernel fungiert.
  • Es spricht nichts dagegen, auch komplexere Supervisor-Prozesse von einfacheren Error-Kernel-beinhaltenden Meta-Supervisor-Prozessen überwachen zu lassen.

Dieses Entwurfsmuster wurde zusammen mit der Programmiersprache Erlang (“Ericson Language”) und dem damit verbundenen Framework OTP entwickelt1. Es ist im Telekommunikationsumfeld weit verbreitet. Tipp: Moderne Sprachvariante: Elixir2

Mit diesem Ansatz werden ultra-robuste Systeme für den produktiven Betrieb kritischer Anwendungen entwickelt.

11.2.4 Design Prinzipien für Aktoren

  • Ein Aktor sollte nur eine klar abgegrenzte Aufgabe haben (“single responsibility”-Prinzip)
  • Spezifische Supervisor: Ein Supervisor sollte nur genau eine Art von Worker überwachen.
  • Einfacher Error Kernel: Die Wurzel in der Überwachungshierarchie sollte so einfach wie möglich sein, um Fehler zu vermeiden, die nicht abgefangen werden können.
  • Fehler-Zonen: Fehler sollten sich nur auf einen abgegrenzten Teil der Hierarchie von Supervisors/Workers auswirken.

11.3 Akka (Java API)

11.3.1 Auf Nachrichten reagieren

record Greeting(String from) {}

public class DemoMessagesActor extends AbstractLoggingActor {
    @Override
    public Receive createReceive() {
        return receiveBuilder()
            .match(Greeting.class, msg -> {
                log().info("greeted by {}", msg.from());
            })
            .build();
    }
}

Dieser Aktor kann auf Nachrichten vom Typ Greeting reagieren, der hier als record implementiert ist. Falls eine solche Nachricht aus der Mailbox des Aktors gelesen wird, wird sie mit der Methode, die hier als Lambda-Ausdruck angegeben wurde, verarbeitet.

Solange die Aktoren lokal sind und nur innerhalb einer JVM-Instanz Nachrichten austauschen, können beliebige Objekte als Nachricht versendet werden. Nächste Woche werden verteilte Aktor eingeführt. Werden zwischen Aktoren, die auf mehreren Rechnern verteilt sind, Nachrichten ausgetauscht, bedeutet dies Netzwerkkommunikation. Dazu müssen Objekte serialisiert werden um sie zu versenden und der Empfänger muss solche Nachrichten deserialiseren, d.h. Objekte aus den Nachrichten erzeugen. In dem Fall müssen Nachrichten das Serializable-Interface implementieren.

public class NewActor extends AbstractActor {
    @Override
    public Receive createReceive() {
        return receiveBuilder()
            .match(Msg1.class, this::receiveMsg1)
            .match(Msg2.class, this::receiveMsg2)
            .match(Msg3.class, this::receiveMsg3)
            .build();
        }
    private void receiveMsg1(Msg1 msg) {...}
    private void receiveMsg2(Msg2 msg) {...}
    private void receiveMsg3(Msg3 msg) {...}
}

Dieser Aktor kann auf Nachrichten der Typen Msg1, Msg2 und Msg3 reagieren.

AbstractActor und AbstractLogging Actor unterscheiden sich dadurch, dass der AbstractLoggingActor einen Logger hat, auf den mit der Methode log aus der Aktor-Instanz zugegriffen werden kann.

Anstatt als Lambda-Ausdruck wie bei DemoMessagesActor werden hier die Aktionen als Funktionsreferenzen mit dem ::-Operator angegeben. Beides ist möglich.

11.3.2 Aktor programmatisch erzeugen

ActorRef parent = 
    Akka.system().actorOf(Props.create(MyActor.class), "myactor");
  • ActorRef ist der Basis-Typ für Aktoren in Akka
  • actorOf ist Factory-Methode zum Erzeugen neuer Aktoren
    • Aktoren werden immer in einem (“Eltern”-) Kontext erzeugt
    • Akka.system() ist “Wurzel”-Kontext
    • Auswahl des Aktor-Typs durch Props.create(): hier Implementierungsklasse des Aktors angeben – z. B. MyActor
    • optional kann als zweiter Parameter von actorOf ein Akka-interner Name angegeben werden, über den der Aktor identifiziert werden kann

Props.create(MyActor.class) ist eine von vielen Möglichkeiten einen MyActor zu instantiieren und im Akka-System “aktiv zu machen”. In diesem Fall würde der Default-Konstruktor von MyActor zur Instantiierung verwendet werden. In manchen Fällen soll aber ein anderer Konstruktor mit Parametern verwendet werden. Dafür und für die Benutzung von spezifischen Factory-Methoden gibt es andere Varianten für die Verwendung von Props, die hier aber nicht weiter betrachtet werden.

11.3.2.1 Kind-Aktor in Eltern-Aktor erzeugen

ActorRef child = 
    getContext().actorOf(Props.create(SubActor.class), "sub");

11.3.3 Aktor-Hierarchie

Der Kontext, in dem ein Aktor erzeugt wurde, bestimmt die Ebene in der Hierarchie, die zwischen den Aktoren herrscht. Es handelt sich aber trotz der Benennung von “Eltern”- und “Kind”-Ebene nicht um eine Vererbungshierarchie. Die Hierarchie zeigt sich in URL’s, mit denen Aktoren referenziert werden können.

getContext() würde hier in einer Methode in parent aufgerufen werden. Das Ergebnis wäre dann der Kontext des “myactor”-Aktors. Wenn in diesem Kontext ein weiterer Aktor “sub” mit actorOf erzeugt wird, ist dessen Kontext dem vorigen untergeordnet.

In der obigen Abbildung sind drei Ebenen von Aktoren dargestellt. Mit getParent() kann man von einem ActorContext ausgehend die ActorRef des Eltern Aktors bekommen und mit getChildren eine Liste der ActorRef der Kinder.

Mit getPath() kann zu einer ActorRef die URL ermittelt werden (ActorPath).

Von seinem Kontext ausgehend kann ein Agent mit getSelf() seine eigene ActorRef ermitteln.

Bei Akka können die Aktoren in einem Netz über mehrere Rechner verteilt werden. Über die URL können wie in der obigen Abbildung dargestellt dann auch entfernte (“remote”) Aktoren referenziert werden. Das Schema der URL ist dann nicht akka, sondern akka.tcp. Außerdem wird dann in der URL der Host Name des Rechners angegeben, auf dem das Aktor-System läuft, (im Beispiel host) sowie die Portnummer (im Beispiel 2552) und der Name des Aktor-Systems selbst (im Beispiel sys).3

11.3.4 Nachrichtenversand

Man kann zwei unterschiedliche Dialogarten bzw. “Sprechakte” zwischen Aktoren unterscheiden: tell und ask.

11.3.4.1 “Fire-and-Forget”-Versand mit “tell”

“Fire-and-Forget” soll andeuten, dass bei diesem Sprechakt nur gesendet wird. Eine etwaige Antwort ist strukturell nicht direkt mit dem vorausgegangenem Senden verknüpft. Inhaltlich kann der Sprechakt “Frage-Antwort” (“Request-Reply”), aber durch zwei “tells” abgebildet werden. Wenn in dem Antwort-“tell” die Frage wiederholt wird, hat der Empfänger der Antwort auch keinen Bedarf die Antwort dem vorigen Frage-“tell” zuzuordnen.

target.tell("Hello", getSelf());
getSender().tell(new Messages.ResultMsg(result), getSelf());
  • target ist eine ActorRef, an deren Aktor die Nachricht gesendet werden soll.
  • Die Nachricht selbst (hier im Beispiel der String “Hello”) kann von einem beliebigen Typ sein. Sollte das Aktor-System auf mehrere Rechner verteilt sein, muss das Objekt, das als Nachricht versendet werden soll, serialisiert werden, um es über TCP zu versenden. Die Voraussetzung dafür ist, dass die Klasse des Objekts das Interface Serializable implementiert.
  • getSelf() -> Der Aktor, der dies aufruft. Ergebnis ist vom Typ ActorRef.
  • getSender() -> Der Aktor, der die Nachricht gesendet hat, die gerade verarbeitet wird. Ergebnis ist vom Typ ActorRef.
  • tell() -> Methode von ActorRef. Der Aktor, an dem diese Methode aufgerufen wird, bekommt eine Nachricht zugesandt (in die Mailbox geschrieben).
    • Die Nachricht ist der erste Parameter der Methode. Der Typ kann beliebig sein, sollte aber vom Empfänger verarbeitet werden. Sonst wird sie in der “Deadletter-Mailbox” gespeichert.
    • Der zweite Parameter ist der Absender der Nachricht (vom Typ ActorRef).
    • Das muss nicht der Aktor sein, der die Nachricht verschickt. Sondern derjenige, der die Antwort auf diese Nachricht erhalten soll (der beim Empfänger mit getSender() ermittelt wird).

11.3.4.2 “Request-Reply”-Versand mit “ask”

Das Ergebnis des Versands von Nachrichten mit ask ist ein CompletableFuture. Wenn die erwartete Antwort später eintrifft, wird der Inhalt des darin enthaltenen Future realisiert.

import static akka.pattern.PatternsCS.ask;

CompletableFuture<Object> future1 = ask(actorA, "request 1", 1000);
CompletableFuture<Object> future2 = ask(actorB, "request 2", 1000);
CompletableFuture<Result> transformed =
CompletableFuture.allOf(future1, future2).thenApply(v -> {
    String x = (String) future1.join();
    String s = (String) future2.join();
    return new Result(x, s);
});
  • Niemals ein CompletableFuture als Nachricht versenden: Wenn Aktoren auf unterschiedlichen Rechnern sind, müsste das Future zum Versand serialisiert werden, was nicht funktioniert.

11.3.5 Design-Prinzipien für Nachrichtenversand

  • “Fire-and-Forget” statt “Request-Reply”: Nachrichten sollten tendenziell eher nicht auf Aufforderung versendet werden, sondern aufgrund eines Ereignisses/situativen Zustands.
  • tell vor ask bevorzugen: mit tell kann genauso ein Request-Reply Kommunikationsmuster umgesetzt werden: In vielen Fällen ist es nicht erforderlich eine Korrelation zwischen Anfrage und Antwort herzustellen.
    • besser: Frage in Antwort wiederholen (bzw. wesentliche Elemente daraus), dann kann der Empfänger sie unabhängig von seiner Anfrage interpretieren
    • ermöglicht auch das Antworten ans andere Aktoren als den ursprünglichen Anfrager

11.3.6 Monitoring

Die folgende Klasse ist ein Beispiel für einen Aktor, der einen anderen Aktor als “Supervisor” überwacht.

In diesem Beispiel wird beim Erzeugen eines WatchActors ein Kind namens “listener” vom Typ ListenerActor erzeugt. Im Konstruktor wird dann der erzeugende WatchActor zum “Supervisor” dieses Kinds gemacht.

public class WatchActor extends AbstractActor {
    ActorRef child = getContext().actorOf(
            Props.create(ListenerActor.class), "listener");
    ActorRef lastSender;
    public WatchActor() {
        getContext().watch(this.child);
    }
    @Override
    public Receive createReceive() {
        return receiveBuilder() 
            .match(PoisonPill.class, (msg) -> {
                getContext().stop(this.child);
                this.lastSender = getSender(); })
            .match(Terminated.class, (msg) -> {
                if (msg.getActor() == this.child) {
                    this.lastSender.tell("finished", null);
                } })
            .build();
    }
}
  • listener” wird als Kind von WatchActor erzeugt (actorOf im Kontext des Aktors mit der Klasse WatchActor)
  • WatchActor wird durch watch(this.child) Supervisor von “listener
  • Supervisors werden durch Terminated.class über Ende von überwachten Aktoren informiert

Erhält der WatchActor eine Nachricht vom Typ PoisonPill, wird das Kind gestoppt. Da das Kind vom WatchActor überwacht wird, wird der WatchActor über das Ende des Kinds informiert, wenn es (in der Zukunft) eingetreten ist. Dies geschieht dadurch, dass die Akka-Infrastruktur eine Terminated-Nachricht erzeugt, in der die ActorRef auf das Kind als Property namens Actor enthalten ist.

Wird solch eine Terminated-Nachricht über das Kind “listener” vom WatchActor empfangen, teilt der WatchActor dies dem Sender der PoisonPill mit, indem der String “terminated” zurücksendet.

11.3.7 Router

11.3.7.1 Abgrenzung Router zu Aktor

  • Router ähnlich zu Aktoren
  • kein eigener Zustand und Verhalten
  • Eingang für eine Reihe nachgeordneter Aktoren (“Routees”)

11.3.7.2 Routee

  • in der Regel gleichartig (z. B. von derselben Klasse)
  • verarbeiten prinzipiell dieselben Nachrichtentypen
  • Router kennt seine Routees

11.3.7.3 Nachrichtenversand über Router

  • Nachricht an Routee wird nicht direkt geschickt, sondern über Router (“route”)
  • Router ändert Nachricht nicht, Absender bleibt ursprünglicher Sender
  • Router entscheidet aufgrund Entscheidungslogik, an welchen Routee weitergeleitet wird
  • Router dient als “Load-Balancer” für Routees

Der Zweck von Routern ist es, Nachrichten zwischen einer Gruppe von möglichen Aktoren (“Routees”) ähnlich einem Load-Balancer zu verteilen. Router ähneln insofern Aktoren, dass Sie Nachrichten empfangen und an andere Aktoren weiterleiten. Allerdings sind sie dabei effizienter, weil Sie nicht die Standardstrukturen bspw. von Mailboxes verwenden.

Router brauchen keinen anwendungsspezifischen eigenen Zustand oder Verhalten sondern dienen lediglich als wiederverwendbarer Baustein zum Eingang für eine Reihe nachgeordneter Aktoren.

Die nachgeordneten Aktoren (“Routees”) sind in der Regel gleichartig (z. B. von derselben Klasse) und verarbeiten prinzipiell dieselben Nachrichtentypen.

Der Router ändert die Nachricht nicht, der Absender bleibt der ursprüngliche Sender. Wenn ein Routee eine Nachricht versendet, sollte der Router als Absender gesetzt werden.

11.3.7.4 Eingebaute Router-Typen

Der Router entscheidet aufgrund einer eigenen Entscheidungslogik, an welchen Routee die Nachricht weitergeleitet werden soll. Die folgenden Entscheidungslogiken für Router sind in Akka verfügbar:

11.3.8 Round-Robin Routing (RoundRobinRoutingLogic)

Nachrichten werden “reihum” an die zur Verfügung stehenden Routees gesendet. Man kann dabei nicht beeinflussen, welcher Routee der erste in der Reihenfolge ist.

Allen (2013, 4)

Allen (2013, 4)
akka.routing.*
    >> RoundRobinRoutingLogic
       RandomRoutingLogic
       SmallestMailboxRoutingLogic
       BroadcastRoutingLogic
       ScatterGatherFirstCompletedRoutingLogic
       TailChoppingRoutingLogic
       ConsistentHashingRoutingLogic

Dieser Router ist besonders geeignet, wenn die zu verteilenden Aufgaben, die hinter den weitergeleiteten Nachrichten stehen, mit einem ähnlichen Aufwand gelöst werden und wenn die Routees vergleichbare oder gemeinsam geteilte Ressourcen zur Verfügung haben. Falls die Aufgaben oder die Ressourcen jedoch ungleichmäßig sind, entsteht eine «Unwucht» bei der Verarbeitung.

11.3.9 Random Routing (RandomRoutingLogic)

Nachrichten werden an einen zufällig ausgewählten Routee gesendet.

Allen (2013, 3)

Allen (2013, 3)
akka.routing.*
       RoundRobinRoutingLogic
    >> RandomRoutingLogic
       SmallestMailboxRoutingLogic
       BroadcastRoutingLogic
       ScatterGatherFirstCompletedRoutingLogic
       TailChoppingRoutingLogic
       ConsistentHashingRoutingLogic

11.3.10 Smallest Mailbox Routing (SmallestMailboxRoutingLogic)

Nachrichten werden zufällig an einen der Routees mit der geringsten Anzahl von wartenden (unverarbeiteten) Nachrichten in der Mailbox gesendet. Allerdings kann solch ein Router nicht auf die Mailbox-Größe entfernter Routees zugreifen. Deshalb haben entfernte Aktoren bei dieser Routing-Logik immer die geringste Priorität.

Allen (2013, 5)

Allen (2013, 5)
akka.routing.*
       RoundRobinRoutingLogic
       RandomRoutingLogic
    >> SmallestMailboxRoutingLogic
       BroadcastRoutingLogic
       ScatterGatherFirstCompletedRoutingLogic
       TailChoppingRoutingLogic
       ConsistentHashingRoutingLogic

Selbst wenn eingehende Nachrichten immer an die kleinste Mailbox gesendet werden, gibt es keine Gewähr dafür, ob die Aufgaben hinter den Nachrichten einer volleren Mailbox nicht in Summe schneller abgearbeitet sind.

11.3.11 Broadcast Routing (BroadcastRoutingLogic)

Nachrichten werden vervielfältigt und jeder Routee erhält ein Exemplar der zu routenden Nachricht.

Allen (2013, 6)

Allen (2013, 6)
akka.routing.*
       RoundRobinRoutingLogic
       RandomRoutingLogic
       SmallestMailboxRoutingLogic
    >> BroadcastRoutingLogic
       ScatterGatherFirstCompletedRoutingLogic
       TailChoppingRoutingLogic
       ConsistentHashingRoutingLogic

11.3.12 Scatter Gather First Completed Routing (ScatterGatherFirstCompletedRoutingLogic)

Wie bei der BroadcastRouterLogic werden Nachrichten an alle Routees weitergeleitet. Hier wird aber speziell das “ask”-Kommunikationsmuster unterstützt: Die zurückgegebene Antwort ist die des ersten der Routees, der eine Antwort zurückgibt. Der Anwendungszweck ist bei zeitkritischen Aufgaben, wenn mehrere Aktoren parallel dasselbe Problem zu lösen versuchen. Das am schnellsten erzeugt Ergebnis wird benutzt, die anderen werden ignoriert.

Allen (2013, 7)

Allen (2013, 7)
akka.routing.*
       RoundRobinRoutingLogic
       RandomRoutingLogic
       SmallestMailboxRoutingLogic
       BroadcastRoutingLogic
    >> ScatterGatherFirstCompletedRoutingLogic
       TailChoppingRoutingLogic
       ConsistentHashingRoutingLogic

Die folgenden Gründe können dazu führen, dass die Routees unterschiedlich lange für die Bearbeitung derselben Aufgabe brauchen:

  • Jeder Routee verwendet einen anderen Lösungsweg oder Algorithmus.
  • Eine Zufallskomponente ist bei der Lösung beteiligt.
  • Verteilte Ressourcen mit unterschiedlicher Auslastung oder Leistungsfähigkeit werden zur Lösung (also zur Ausführung der Routees) benutzt.

11.3.13 Router-Verwendung

Das folgende Listing zeigt exemplarisch, wie ein Router mit RoundRobinRoutingLogic-Verhalten Nachrichten zwischen fünf Routees vom Typ Worker aufteilt: Statt Nachrichten mit tell oder ask zu senden, werden sie mit route über den Router router verteilt:

var routees = new ArrayList<Routee>();
for (var i = 0; i < 5; i++) {
    var routee = getContext().actorOf(Props.create(Worker.class));
    getContext().watch(routee);
    routees.add(new ActorRefRoutee(routee));
}
var router = new Router(new RoundRobinRoutingLogic(), routees);

// ...
router.route(message, getSelf());

11.3.14 Kopplung von Router und Routees

11.3.14.1 Gruppe

Im Fall einer Router-Group sind Router und Routees weniger eng gekoppelt:

  • Der Router erzeugt “seine” Routees-Aktoren nicht selber, sondern bekommt sie mitgeteilt.
  • Er überwacht die Routees nicht selber.

11.3.14.2 Pool

Im Fall eines Router-Pools sind Router und Routees enger gekoppelt als bei einer Gruppe:

  • Der Router erzeugt “seine” Routees-Aktoren selber und überwacht sie.
  • Sollte ein Routee terminieren, entfernt der Router sie aus seiner Routee-Liste.
ActorSystem sys = ActorSystem.create("sys");
ActorRef r =  sys.actorOf(
      new RoundRobinPool(5).props(Props.create(Worker.class)), "router");

11.3.14.3 BalancingPool

Beim BalancingPool teilen alle Routees eine Mailbox.

Die Arbeit (Nachrichten in der Mailbox) werden also optimal auf die zur Verfügung stehenden (Worker-) Ressourcen aufgeteilt: Kein Routee muss auf Nachrichten warten während andere Routees mit der Abarbeitung der Nachrichten nicht hinterherkommen.

11.3.15 Exkurse4

11.3.15.1 Konfigurationsdateien

Neben der programmatischen Verwendung von Routern kann auch der konfigurative Ansatz verwendet werden.

Das config-Framework ist ein moderner Ersatz von Properties-Dateien, ist aber nicht Teil des Java-Standard API’s, sondern muss als Library eingebunden werden. Es wird von Akka intensiv genutzt. config ist daher Teil der Abhängigkeiten (“dependencies”) von Akka.

import com.typesafe.config.ConfigFactory;

public class MainClassOfApp {

    public static void main(String... args) {
        var system = ActorSystem.create("main", ConfigFactory.load("config"));
        //…
    }
}

Sucht die Konfigurationsdatei config.conf im current working directory (das Verzeichnis, von dem aus die Anwendung gestartet wird (nicht unbedingt das Verzeichnis, in dem die Klasse liegt).

Bei Verwendung von Maven: src/main/resources/config.conf

11.3.15.2 Router-Gruppe über Konfiguration erzeugen

akka.actor.deployment {
  router {
    router = round-robin-group
    routees.paths = ["/user/worker1", "/user/worker2", "/user/worker3"]
  }
}
public class Main {
    public static void main(String... args) {
        var sys = ActorSystem.create("sys", ConfigFactory.load("config"));
        sys.actorOf(Props.create(WorkerActor.class), "worker1");
        sys.actorOf(Props.create(WorkerActor.class), "worker2");
        sys.actorOf(Props.create(WorkerActor.class), "worker3");
        var r = sys.actorOf(FromConfig.getInstance().props(),  "router");
    }
}

11.3.15.3 Verteilte Akka-Systeme

akka {
    actor {
        provider = remote
    }
    remote {
        enabled-transports = ["akka.remote.netty.tcp"]
        netty.tcp {
            hostname = "10.136.117.1"
            port = 4711
        }
    }
}

Die obige Konfiguration zusammen mit MainClassOfApp (s.o.) würde auf dem Rechner 10.136.117.1 ein Akka-System mit der URL akka.tcp://main@10.136.117.1:4711 starten.

akka {
    actor {
        provider = remote
    }
    remote {
        enabled-transports = ["akka.remote.netty.tcp"]
        netty.tcp {
            hostname = "10.136.117.2"
            port = 8080
        }
    }
}

Die obige Konfiguration zusammen mit MainClassOfApp (s.o.) würde auf dem Rechner 10.136.117.2 ein Akka-System mit der URL akka.tcp://main@10.136.117.2:8080 starten.

Um einen entfernten Aktor von 10.136.117.1 aus auf 10.136.117.2 zu finden, müsste man auf Rechner 10.136.117.1 folgendes ausführen:

ActorSelection remoteActor = 
    getContext().actorSelection("akka.tcp://sys1@10.136.117.2:8080/user/worker");
remoteActor.tell("Hallo von 10.136.117.1");

Die Nachricht “Hallo von 10.136.117.1” wird dann an den Aktor /user/worker auf dem Rechner 10.136.117.2 geschickt.

Um einen entfernten Aktor von 10.136.117.1 aus auf 10.136.117.2 zu erzeugen, müsste man auf Rechner 10.136.117.1 folgendes ausführen:

ActorRef remoteActor = system.actorOf(Props.create(WorkerActor.class), "worker");
remoteActor.tell("Hallo von 10.136.117.1", ActorRef.noSender());

Aktor /user/worker wird dann auf dem Rechner 10.136.117.2 erzeugt und die Nachricht “Hallo von 10.136.117.1” wird an ihn geschickt.

11.3.15.4 Entfernte Aktoren über Konfiguration

Bei folgender Konfiguration (Ausschnitt) auf 10.136.117.1 würde ein lokaler Zugriff auf den Aktor namens remote/... (Name beginnt mit remote/) effektiv an den entsprechenden Aktor auf dem Akka-Knoten auf 10.136.117.2 erfolgen:

akka {
  actor {
    deployment {
      "remote/*" {
        remote = "akka.tcp://sys1@10.136.117.2:8080"
      }
    }
  }
}

Das Wildcard-Symbol * funktioniert nur innerhalb von Anführungsstrichen. Ein partielles Matching wie z. B. worker-* funktioniert leider nicht. Es kann vielmehr immer nur ein Aktor-Name (was zwischen “/” steht) ersetzt werden.

11.3.15.5 Entfernte Aktoren und Nachrichten

Nachrichten-Objekte müssen von einer serialisierbaren Klasse instanziiert werden, damit sie als Byte-Abfolge über eine Netzwerkverbindung versendet werden können:

import java.io.Serializable;

public class PleaseCleanupAndStop implements Serializable {
    private static final long serialVersionUID = 7273183276322548603L;
}

Die serialVersionUID ist eine eindeutige Zahl, mit der die Klasse beim Empfänger wiedererkannt werden kann (hilft bei Doppelbenennungen und Versionskonflikten).

11.3.15.6 Router-Gruppe

Das folgende Listing zeigt einen Teil der Konfiguration für einen Router.

akka.actor.deployment {
  /parent/routerGroup {
    router = round-robin-group
    nr-of-instances = 5
  }
}

Aus solch einer Konfiguration kann der Router zusammen mit seinen Routees programmatisch erzeugt werden:

public class Main {
    public static void main(String... args) {
        var sys = ActorSystem.create("sys", ConfigFactory.load("app"));
        var r = sys.actorOf(FromConfig.getInstance().props(
                              Props.create(Worker.class)),  "router");
    }
}

über den dann wieder Nachrichten wie im obigen Code-Beispiel versendet werden können:

r.route(message, getSelf());

11.3.15.7 Router für verteilte Routees

Der konfigurative Ansatz ermöglicht zudem analog zur Definition eines Routers mit lokalen Routees die effektive Nutzung verteilter Ressourcen. Im folgenden Listing ist die Konfiguration eines RoundRobinRoutingLogic-Routers und einer verteilten Gruppe von drei Routees vom Typ Worker auf den Hosts host1, host2 und host3 dargestellt.

bei jeweils einem Routee auf jedem der drei Knoten host1, host2 und host3:

akka.actor.deployment {
  /parent/remoteGroup {
    router = round-robin-group
    routees.paths = [
      ”akka.tcp://test@host1:4711/user/workers/w1”,
      ”akka.tcp://test@host2:4711/user/workers/w1”,
      ”akka.tcp://test@host3:4711/user/workers/w1”]
  }
}

Als Pool von zehn gleichmäßig auf host2 und host3 verteilten Routees:

akka.actor.deployment {
  /parent/remotePool {
    router = round-robin-pool
    nr-of-instances = 10
    target.nodes = [
      ”akka.tcp://app@host2:4711”, 
      ”akka.tcp://app@host3:4711”]
  }
}