W poprzednim wpisie pokazałem, w jaki sposób możemy zaprojektować obsługę błędów. Jak widać mamy do dyspozycji sporo opcji. Z punktu widzenia AKKA.NET nie jest to jednak tak skomplikowane. Wystarczy przeładować jedną metodę i zwrócić odpowiedni obiekt.
Tak jak w poprzednim wpisie będziemy testować kod na następującym “systemie”:
Dla przypomnienia nasz ApplicationUserActor wygląda następująco:
public class ApplicationUserActor : UntypedActor { private readonly string _userName; public ApplicationUserActor(string userName) { _userName = userName; } protected override void OnReceive(object message) { Console.WriteLine("Received by {0}: {1}", _userName, message); } protected override void PreStart() { Console.WriteLine("{0}: PreStart",_userName); base.PreStart(); } protected override void PostStop() { Console.WriteLine("{0}: PostStop", _userName); base.PostStop(); } protected override void PreRestart(Exception reason, object message) { Console.WriteLine("{0}: PreRestart", _userName); base.PreRestart(reason, message); } protected override void PostRestart(Exception reason) { Console.WriteLine("{0}: PostRestart", _userName); base.PostRestart(reason); } }
Póki co niewiele mamy tam kodu – głównie hooking, które pomogą nam w zrozumieniu propagacji błędów.
Zmodyfikujmy metodę OnReceived tak, aby zasymulować wyjątek:
protected override void OnReceive(object message) { if (message.ToString() == "error") throw new ArgumentException(); Console.WriteLine("Received by {0}: {1}", _userName, message); }
W celu zdefiniowania obsługi błędów wystarczy przeciążyć metodę SupervisorStrategy aktora zarządzającego. Jeśli chcemy więc obsłużyć wyjątek w ApplicationUserActor, wtedy węzeł zarządzający (rodzic) to ApplicationUserControllerActor. Kod:
protected override SupervisorStrategy SupervisorStrategy() { return new OneForOneStrategy((exception) => { if (exception is ArgumentException) return Directive.Restart; return Directive.Escalate; }); }
W przykładzie wybraliśmy strategię OneForOneStrategy, którą opisałem już w poprzednim wpisie. W skrócie, rodzeństwo węzła, który spowodował wyjątek, nie będzie odgrywało tutaj żadnej roli. Wystarczy, że przekażemy wyrażenie lambda, które określa co należy zrobić w zależności od typu wyjątku. W powyższym przykładzie restartujemy aktora. Tak jak napisałem w poprzednim poście, mamy cztery sposoby reakcji:
public enum Directive { Resume, Restart, Escalate, Stop, }
W celu zaprezentowania efektu, stwórzmy dwóch aktorów i wyślijmy serię wiadomości:
var system = ActorSystem.Create("FooHierarchySystem"); IActorRef userControllerActor = system.ActorOf<ApplicationUserControllerActor>("ApplicationUserControllerActor"); userControllerActor.Tell(new AddUser("Piotr")); userControllerActor.Tell(new AddUser("Pawel")); var actor1 = system.ActorSelection("/user/ApplicationUserControllerActor/Piotr"); var actor2 = system.ActorSelection("/user/ApplicationUserControllerActor/Pawel"); Console.ReadLine(); actor1.Tell("Sample message I"); Console.ReadLine(); actor1.Tell("error"); Console.ReadLine(); actor1.Tell("Sample message II"); Console.ReadLine();
Widzimy, że w momencie wystąpienia błędu, aktor został zrestartowany. Ze screenu również można zauważyć, że kolejne wiadomości zostaną przetworzone. Stan wewnętrzny został zrestartowany, ale nie kolejka wiadomości. W celu zademonstrowania, że stan wewnętrzny faktycznie jest wymazywany (ponieważ tworzona jest nowa instancja), dodajmy prywatne pole do klasy:
public class ApplicationUserActor : UntypedActor { private readonly string _userName; private string _internalState; ... }
InternalState jest wyświetlany i ustawiany w OnReceive:
protected override void OnReceive(object message) { Console.WriteLine("{0}:Internal State: {1}",_userName,_internalState); if (message.ToString() == "error") throw new ArgumentException(); _internalState = message.ToString(); Console.WriteLine("Received by {0}: {1}", _userName, message); }
Teraz widzimy, że po wystąpieniu wyjątku, InternalState będzie pusty:
Analogicznie, spróbujmy zmienić dyrektywę restart na resume:
protected override SupervisorStrategy SupervisorStrategy() { return new OneForOneStrategy((exception) => { if (exception is ArgumentException) return Directive.Resume; return Directive.Escalate; }); }
Po uruchomieniu programu, przekonamy się, że stan wewnętrzny nie jest usuwany:
Zmieńmy również strategię na AllForOneStrategy:
protected override SupervisorStrategy SupervisorStrategy() { return new AllForOneStrategy((exception) => { if (exception is ArgumentException) return Directive.Restart; return Directive.Escalate; }); }
Efekt będzie taki, że wszystkie węzły podrzędne zostaną zrestartowane:
Jeśli w jakimś aktorze nie zdefiniujemy strategii obsługi błędów, wtedy domyślna będzie użyta:
protected virtual SupervisorStrategy SupervisorStrategy() { return SupervisorStrategy.DefaultStrategy; }
Domyślna strategia to z kolei OneForOneStrategy.
Warto również przyjrzeć się innym przeciążeniom konstruktora, np.:
/// <summary> /// Applies the fault handling `Directive` (Resume, Restart, Stop) specified in the `Decider` /// to all children when one fails, as opposed to <see cref="T:Akka.Actor.OneForOneStrategy"/> that applies /// it only to the child actor that failed. /// /// </summary> /// <param name="maxNrOfRetries">the number of times a child actor is allowed to be restarted, negative value means no limit, /// if the limit is exceeded the child actor is stopped. /// </param><param name="withinTimeRange">duration of the time window for maxNrOfRetries, Duration.Inf means no window.</param><param name="localOnlyDecider">mapping from Exception to <see cref="T:Akka.Actor.Directive"/></param> public OneForOneStrategy(int? maxNrOfRetries, TimeSpan? withinTimeRange, Func<Exception, Directive> localOnlyDecider) : this(maxNrOfRetries.GetValueOrDefault(-1), (int) withinTimeRange.GetValueOrDefault(Timeout.InfiniteTimeSpan).TotalMilliseconds, localOnlyDecider, true) { }
Widzimy, że oprócz wspomnianego wyrażenia lambda, możemy określić maksymalną liczbę prób oraz przedział czasowy.
Czekam na więcej … 🙂