AKKA.NET – Przykład obsługi błędów

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”:

controller

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();

1
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:
2

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:
3

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:
4

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.

One thought on “AKKA.NET – Przykład obsługi błędów”

Leave a Reply

Your email address will not be published.