Donnerstag, 10. Dezember 2015

Akka.NET Adventskalender – Tür 10

Wenn etwas schief gehen kann wird es schief gehen

Wir haben gestern den theoretischen Fall eines fehlschlagenden (Exception werfenden) Aktors angeschnitten. Was macht man in solchen Situationen? Was kann passieren und wie gehen wir mit solchen Fehlersituationen um?

Un die Situationen, die auftreten können zu analysieren, sehen wir uns die möglichen "Bruchstellen" in unserem Spiel an.
  • Der Game Aktor könnte eine Exception werfen. Das ist nicht unser Problem, weil der Aktor, der das Spiel gestartet hat, dieses Fehlverhalten behandeln muss. Das Standardverhalten ist, den Aktor neu zu starten, wir bekommen also ein neues Spiel, das dann (hoffentlich) glatt läuft.
  • Der Chooser könnte sterben. Würden wir den einfach neu starten lassen, würde er sich eine neue Zufallszahl ausdenken und die Antworten, die der Enquirer erhält wären möglicherweise inkonsistent. In diesem Fall macht es also Sinn, den Chooser und den Enquirer (also alle "Kinder" des Game-Actors) neu zu starten.
  • Der Enquirer fällt aus. Zwar wären seine bisherigen Versuche damit verloren, aber er kann ja nochmal neu anfangen, wenn wir ihn neu starten. Der neue Aktor wird dann die Zahl schon erraten.
Das hört sich nach viel Arbeit an. Die gute Nachricht ist, es hört sich zwar kompliziert an, ist aber ganz einfach. Aber erst einmal müssen wir unsere Aktoren mit Sollbruchstellen ausstatten, damit wir das Fehlverhalten für einen nachfolgenden lauf simulieren können.

Dem Enquirer spendieren wir einen crashCounter, der exakt beim 3. Rateversuch zu plötzlichem Ableben führt, danach aber nie wieder zuschlägt.

public class Enquirer : ReceiveActor
{
 ...
    private static int crashCounter = 3;
 
    ...
    
    private void MakeATry()
    {
        if (--crashCounter == 0)
            throw new InvalidOperationException();
  ...
 }
...

Nachdem wir unser Spiel gestartet haben, wird dieser Aktor wie geplant sterben. Aber danach passiert nichts mehr. Zwar haben wir gehört, dass ein Neustart die Standard Option ist, aber der Aktor erhielt keine Start Nachricht nach dem Neustart. Demnach kam er gar nicht auf die Idee, mit dem Raten neu zu beginnen. Das könnte man mit der PostRestart Methode lösen. (Genau genommen ist das in unserem Fall sogar falsch, nämlich dann wenn beide Aktoren neu gestartet werden, denn dann fallen wir möglicherweise in das anfangs diskutierte Problem. Aber wir wollen uns für diese einfache konstruierte Aufgabe das Leben nicht unnötig kompliziert machen)

protected override void PostRestart(Exception reason)
{
    Console.WriteLine("Enquirer: PostRestart");
    
    // we crashed during processing. So it is wise to Start again
    Self.Tell(new Start());
}

Mit dieser kleinen Anpassung wird die erste Anforderung erfüllt. Aber wie erfüllen wir die zweite Anforderung beide Kinder neu zu starten? Das ist eine Aufgabe, die mit einer SupervisorStragegy gelöst werden kann. Der Standard dafür ist die OneForOneStrategy (Neustart des einen verstorbenen Aktors), wir wählen für die von uns geforderte Anforderung die AllForOneStrategy, mit der wir beim Ableben eines Kindes alle Kind Aktoren neu starten.

Wir passen also die Erzeugung des Chooser Aktors wie folgt an:

chooser = Context.ActorOf(
    Props.Create<Chooser>()
         .WithSupervisorStrategy(
             new AllForOneStrategy(ex => Directive.Restart)),
    "Chooser");

Verwendet man in unserem Fall die kombinierte Logik, kann man je nach Bedarf individuell auf Fehler reagieren. Zugegeben, diese Situationen sind konstruiert und nicht ganz korrekt implementiert, aber ich hoffe, ich konnte das Prinzip dahinter verdeutlichen. Die Änderungen sind wieder im github Repository zu finden. Die dort befindlichen Klassen sind mit reichlich Ausgaben ausgestattet, so dass nachvollziehbar ist, wer wann was macht.

Morgen werden wir uns weitere Anwendungsmöglichkeiten für einzelne Aktoren ansehen.

Keine Kommentare:

Kommentar veröffentlichen