Fortpflanzung auf Aktor-isch
Gestern haben wir uns angesehen, wie wir Nachrichten an einen Aktor senden und dort behandeln können. Ich hatte auch schon erwähnt, dass ein Aktor Kinder erzeugen kann, damit er Arbeit an diese abgeben kann. Wenn wir uns für diesen Weg entscheiden, müssen wir uns der Verantwortung bewusst werden, die wir damit eingehen. Unser Aktor ist verantwortlich für seine Kinder und muss sie kontrollieren, beaufsichtigen und eventuell beenden.
Insgesamt entsteht durch die Aktoren eine Hierarchie, die am besten mit einem Dateisystem vergleichbar ist. Direkt aus dem
ActorSystem erzeugte Aktoren liegen im "
/user" Pfad des Baumes, Kind-Aktoren wie bei verschachtelten Dateisystemen entsprechend an die Eltern-Aktoren angehängt.
Von einem Aktor aus angelegte Kinder werden mittels des sogenannten
Context angelegt. Dem
Context werden wir noch ein paar mal begegnen. Mich persönlich hat er anfangs eher verwirrt, dabei ist die Trennung zwischen Aktor und Context eigentlich ganz logisch. Der Aktor ist ein Objekt, welches das Verhalten und den Zustand repräsentiert. Der Context wird von der Laufzeitumgebung bereit gestellt und enthält infrastrukturelle Dinge wie die Verbindung zu Eltern, Kindern, dem Aktorsystem etc.
Neue Aktoren werden also so erzeugt:
// system == unser Aktor System
// Aktor unter /user anlegen
system.ActorOf( ... );
// wenn wir uns innerhalb eines Aktors befinden
// legen wir so ein Kind an
Context.ActorOf( ... );
Der Aktor Lebenszyklus
Wenn wir eine Hierarchie von Aktoren aufbauen, dann geschieht das im Sinne einer sinnvollen Arbeitsteilung. Jede Ebene operiert auf unterschiedlichem Niveau und delegiert und kontrolliert damit eventuell darunter liegende Aktoren. Wie im täglichen Leben haben höher angesiedelte Aktoren weniger Risiko aber mehr Verantwortung, weiter unten angesiedelte Aktoren ein deutlich höheres Risiko aber kaum Verantwortung. Wenn ein Aktor stirbt, wird er durch ein baugleiches Modell ersetzt. Klingt grausam, ist aber Realität in der Welt der Aktoren.
Damit wir in den diversen Lebenszyklen (starten, neu starten und anhalten) eingreifen können (möglicherweise dürfen wir bestimmte Daten nicht verlieren), bieten Aktoren vier verschiedene Methoden, die wir bereit stellen können. Sie werden durch die Laufzeitumgebung zum gegebenen Zeitpunkt aufgerufen.
*
protected override void PreStart()
*
protected override void PreRestart(Exception reason, object message)
*
protected override void PostRestart(Exception reason)
*
protected override void PostStop()
Etwas genauer steht es in der
Akka.NET Dokumentation.
Um die Ergebnisse der diversen Methodenaufrufe einmal untersuchen zu können, möchte ich Dich bitten, ein kleines Konsolen-Projekt anzulegen und das nachfolgende Programm darin abzulegen. Anschließend werden air einen Aktor mit einem Kind anlegen, das leider öfter Exceptions erzeugt. Standardmäßig wird solch ein Aktor neu gestartet.
using System;
using Akka.Actor;
using System.Threading;
namespace AkkaSuperVision
{
class MainClass
{
public static void Main(string[] args)
{
var system = ActorSystem.Create("Hooks");
var supervisor = system.ActorOf(
Props.Create<Supervisor>(),
// Props.Create<Supervisor>().WithSupervisorStrategy( ... ),
"Supervisor");
for (var i=0; i<500; i++)
supervisor.Tell("message " + i);
Thread.Sleep(500);
Console.WriteLine("Press [enter] to continue...");
Console.ReadLine();
system.Shutdown();
system.AwaitTermination();
}
}
}
Unser etwas zickiges Kind könnte so wie hier aussehen:
using System;
using Akka.Actor;
namespace AkkaSuperVision
{
public class Child : ReceiveActor
{
private int nrMessagesHandled;
public Child ()
{
nrMessagesHandled = 0;
Receive<string>(s => HandleStringMessage(s));
}
protected override void PreStart()
{
Console.WriteLine("PreStart Actor '{0}'", Self.Path.Name);
}
protected override void PreRestart(Exception reason, object message)
{
Console.WriteLine("PreRestart Actor '{0}', reason: {1}, message: {2}",
Self.Path.Name, reason.Message, message);
}
protected override void PostRestart(Exception reason)
{
Console.WriteLine("PostRestart Actor '{0}', reason: {1}",
Self.Path.Name, reason.Message);
}
protected override void PostStop()
{
Console.WriteLine("PostStop Actor '{0}'", Self.Path.Name);
}
private void HandleStringMessage(string message)
{
Console.WriteLine("Received: '{0}'", message);
if (++nrMessagesHandled >= 3)
throw new InvalidMessageException("haha");
}
}
}
Und natürlich brauchen wir einen überwachenden Aktor. Er wird ebenfalls eine String-Nachricht erhalten und sie einfach an das (zickige) Kind weiterleiten.
using System;
using Akka.Actor;
namespace AkkaSuperVision
{
public class Supervisor : ReceiveActor
{
public IActorRef child;
public Supervisor()
{
child = Context.ActorOf(Props.Create<Child>(),"Child");
Context.Watch(child);
Receive<string>(s => HandleStringMessage(s));
}
private void HandleStringMessage(string message)
{
child.Tell(message);
}
// protected override SupervisorStrategy SupervisorStrategy()
// {
// return new OneForOneStrategy(
// 10,
// TimeSpan.FromSeconds(10),
// exception =>
// {
// return Directive.Restart;
// }
// );
// }
}
}
Wenn Du das Kommandozeilen Programm startest, wirst Du einen Kind-Aktor erleben, der die String-Nachrichten empfängt, aber bei jeder 3. Nachricht wie beabsichtigt, eine Exception auslöst. Aber er wird automatisch neu gestartet, ohne dass wir uns darum kümmern müssen. Dahinter steckt die sogenannte
OneForOne Strategie. Wenn wir eine eigene Strategie hinterlegen möchten, können wir das in einem Aktor durch Überladen der
SupervisorStrategy() Methode oder beim Erzeugen des Aktors erledigen. Damit können wir das Verhalten beim Sterben von Kind-Aktoren entsprechend anpassen.
Das Ergebnis beim Programmlauf könnte so aussehen:
PreStart Actor 'Child'
Received: 'message 0'
Received: 'message 1'
Received: 'message 2'
PreRestart Actor 'Child', reason: haha, message: message 2
PostRestart Actor 'Child', reason: haha
...
Received: 'message 497'
PreRestart Actor 'Child', reason: haha, message: message 497
PostRestart Actor 'Child', reason: haha
Received: 'message 498'
Received: 'message 499'
Press [enter] to continue...
"Stimmt nicht ganz" wirst Du gleich sagen. Hmmm, ok, ein wenig geschummelt habe ich. Standardgemäß werden alle Exceptions mitgeloggt und die Console ist das normalerweise eingestellte Ziel für die Log Ausgaben. Um es abzuschalten, könntest Du deinem Projekt die nachfolgende Konfigurations-Datei beisteuern. Akka.NET verwendet eine eigene Syntax, die innerhalb der Tags "akka" und "hocon" eingebaut sind. Hier kann z.B. das Logging temporär deaktiviert werden. Für praktische Projekte ist das nicht empfehlenswert, aber für unsere Experimente durchaus legitim
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="akka" type="Akka.Configuration.Hocon.AkkaConfigurationSection, Akka" />
</configSections>
<akka>
<hocon>
<![CDATA[
akka {
loggers = ["Akka.Event.StandardOutLogger"]
log-config-on-start = off
stdout-loglevel = OFF
loglevel = DEBUG
actor {
debug {
# receive = on
# autoreceive = on
# lifecycle = on
# event-stream = on
# unhandled = on
}
}
]]>
</hocon>
</akka>
</configuration>
Die kompletten Details zur Konfiguration findest Du in der
Akka.NET Dokumentation.
Zeit für Experimente. Was passiert, wenn Du die SupervisorStrategy() Methode oben auskommentierst? Kannst Du Dir das Verhalten erklären? Wie müsstest Du den Code verändern, um das erwartete Verhalten zu erreichen? Und zum Schluss könntest Du mit der WithSuperVisorStrategy() Methode experimentieren – das wäre zumindest der kürzeste Weg!
Morgen werden wir uns einiges rund um Nachrichten ansehen