Skocz do zawartości

AKKA.NET: Hierarchia aktorów


DevStart Blogi

Recommended Posts

Hierarchia aktorów to kolejny bardzo ważny element w projektowaniu systemów rozproszonych, bazujących na aktorach.

Przede wszystkim pomaga w tworzeniu atomowych aktorów. Dzięki hierarchii, pojedynczy aktor może zawierać na tyle mało logiki, że jesteśmy w stanie wykonać operację wielowątkową bez użycia blokad.

Z poprzedniego artykułu o Reactive Manifesto, pamiętamy, że obsługa błędów (resilent) oraz skalowalność muszą być zagwarantowane w systemach reaktywnych. Dzięki hierarchii aktorów bardzo łatwo możemy izolować awarie pojedynczego węzła od innych. Ponadto, ze względu na relacje między różnymi aktorami, możemy dobrać odpowiednio strategie obsługi błędów.

Hierarchia aktorów stanowi drzewo relacji. Każdy węzeł ma rodzica, który stanowi jego zarządcę. To od niego należy, czy w momencie wystąpienia błędu, wszystkie węzły potomne zostaną anulowane czy tymczasowo zawieszone.

Zasada jest taka więc, że tworzymy coraz to nowe poziomy w drzewie aktorów, aż w do momentu, gdy logika w pojedynczym aktorze jest wystarczająco prosta i odizolowana.

W systemie AKKA.NET każdy węzeł ma zarządce i każdy z nich może być również zarządcą swoich potomków.  W AKKA.NET istnieją również systemowe węzły:

root

Aktor / jest bazowym węzeł dla całego systemu. To od niego zaczyna się hierarchia. Później mamy /user oraz /system. Każdy aktor zdefiniowany przez nas, będzie podlegał /user. Jeśli /user zdecyduje się na anulowanie podrzędnych węzłów, nasi aktorzy zostaną anulowani. Z kolei /system, to inny bazowy węzeł, dla celów czysto systemowych takich jak obsługa błędów czy wykonywanie logów.

Załóżmy, że mamy system składający się z dowolnej liczby użytkowników. Nie ma w tej chwili znaczenia co to dokładnie za system. Wiemy, że jednym z aktorów będzie po prostu “ApplicationUser”.  Jednym z rozwiązań, byłoby stworzenie następującej hierarchii:

h

Powyższa hierarchia, składająca się z pojedynczego aktora nie jest zbyt dobra.  Lepiej wprowadzić koncept ApplicationUserController, który będzie przechowywał użytkowników oraz aktorów. ApplicationUserController będzie zatem zarządzał aktorami ApplicationUser:

controller

W momencie zalogowania się nowego użytkownika, kolejny ApplicationUser jest tworzony przez ApplicationUserController.

Przejdźmy zatem do implementacji. Zaczniemy od ApplicationUser:

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

ApplicationUser w naszym przypadku będzie po prostu wyświetlał przychodzące wiadomości. Ponadto przeciążyłem hook’i opisane w poprzednim poście. Ułatwią one później śledzenie zachowania systemu. Każdy ApplicationUser przechowuje nazwę użytkownika, dzięki której później będziemy w stanie rozróżniać poszczególne instancje aktora.

Kolejny aktor będzie służył do zarządzania ApplicationUser. Często określa się go miastem strażnika (guardian), ponieważ to on jest odpowiedzialny za zarządzanie, jak i obsługę błędów (więcej o tym w następnym wpisie). Kod:

public class ApplicationUserControllerActor : ReceiveActor
    {
        public ApplicationUserControllerActor()
        {
            Receive<AddUser>(msg => AddUser(msg));


        }

        private void AddUser(AddUser msg)
        {
            Console.WriteLine("Requested a new user: {0}", msg.UserName);

            IActorRef newActorRef =
                Context.ActorOf(Props.Create(() => new ApplicationUserActor(msg.UserName)), msg.UserName);

        }

        protected override void PreStart()
        {
            Console.WriteLine("ApplicationUserControllerActor: PreStart");
            base.PreStart();
        }

        protected override void PostStop()
        {
            Console.WriteLine("ApplicationUserControllerActor: PostStop");
            base.PostStop();
        }

        protected override void PreRestart(Exception reason, object message)
        {
            Console.WriteLine("ApplicationUserControllerActor: PreRestart");
            base.PreRestart(reason, message);
        }

        protected override void PostRestart(Exception reason)
        {
            Console.WriteLine("ApplicationUserControllerActor: PostRestart");
            base.PostRestart(reason);
        }
    }

Aktor obsługuje wyłącznie wiadomość AddUser, która wygląda następująco:

    public class AddUser
    {
        public string UserName { get; private set; }

        public AddUser(string userName)
        {
            UserName = userName;
        }
    }

W momencie otrzymania AddUser, dodany jest nowy aktor podrzędny za pomocą:

Context.ActorOf(Props.Create(() => new ApplicationUserActor(msg.UserName)), msg.UserName);

Wywołanie ActorOf w kontekście aktora A, spowoduje utworzenie aktora B, który jest podrzędny względem A. Innymi słowy, ApplicationUserActor podlega ApplicationUserController, który z kolei podlega /user. Jak wiemy z początku wpisu, /user jest systemowym aktorem, który zawsze istnieje. Być może, nie potrzebnie nazywałem swoich aktorów również “User”. W każdym, razie /user jest systemowy i nie ma nic wspólnego z logiką biznesową danej aplikacji. Z kolei ApplicationUser oraz ApplicationUserController zostały stworzone przez nas za pomocą powyższego kodu.

Przejdźmy teraz do testowania. Korzeń systemu tworzymy w znany już sposób:

var system = ActorSystem.Create("FooHierarchySystem");

IActorRef userControllerActor=system.ActorOf<ApplicationUserControllerActor ("ApplicationUserControllerActor");

Następnie wysyłamy dwie wiadomości AddUser, w celu dodania dwóch nowych użytkowników, a co za tym idzie, również dwóch nowych aktorów:

userControllerActor.Tell(new AddUser("Piotr"));
userControllerActor.Tell(new AddUser("Pawel"));

Na ekranie zobaczymy, że najpierw ApplicationUserController zostały stworzony, potem dwóch użytkowników zostało dodanych, a po pewnych czasie hooki OnPreStart dla nowych aktorów zostały wywołane:

1

Następnie, chcemy wysłać wiadomość do nowo utworzonych aktorów:

Console.ReadLine();
Console.WriteLine("Sending a message to ApplicationActor(Piotr)...");
var actor1 = system.ActorSelection("/user/ApplicationUserControllerActor/Piotr");
actor1.Tell("Testing actor 1");

Console.ReadLine();
Console.WriteLine("Sending a message to ApplicationActor(Pawel)...");
var actor2 = system.ActorSelection("/user/ApplicationUserControllerActor/Pawel");
actor2.Tell("Testing actor 2");

Warto zwrócić jak uzyskujemy dostęp do wcześniej już stworzonego aktora:

system.ActorSelection("/user/ApplicationUserControllerActor/Piotr");

Więcej o definiowaniu ścieżek napiszę innym razem. W najprostszej postaci zawierają jednak one serie nazw aktorów. Korzeniem zawsze jest /user. Następnie stworzyliśmy pojedynczą instancję ApplicationUserControllerAcrtor. Ostatnią częścią jest nazwa instancji ApplicationUser. W naszym przypadku, jako nazwę tej instancji podaliśmy nazwę użytkownika. Dla przypomnienia:

                Context.ActorOf(Props.Create(() => new ApplicationUserActor(msg.UserName)), msg.UserName);

Drugi parametr, metody Props.Create to nazwa aktora. Ponadto, jak widzimy, hierarchia aktorów nie pełni roli routing’u wiadomości. Nie musimy wysyłać wiadomości do korzenia systemu. Wystarczy zaznaczyć konkretnego aktora i komunikować się bezpośrednio z nim. Relacje aktorów mają wyłącznie znaczenie dla obsługi błędów (zobaczymy w następnym wpisie), jak i zarządzania czasem życia instancji.

W tej chwili, na ekranie powinniśmy zobaczyć następujące logi:
2

Na koniec, zobaczmy jak aktorzy zachowują się w momencie zamykania systemu:

            Console.ReadLine();
            Console.WriteLine("Requesting the system shutdown.");
            system.Shutdown();
            system.AwaitTermination();

Nie trudno domyślić się, że najpierw zostanie zamknięty ApplicationUserController, a dopiero potem dwie instancje ApplicationUser. Całość kodu do testowania:

 class Program
    {
        static void Main(string[] args)
        {
            var system = ActorSystem.Create("FooHierarchySystem");
            IActorRef userControllerActor =
                system.ActorOf<ApplicationUserControllerActor>("ApplicationUserControllerActor");

            userControllerActor.Tell(new AddUser("Piotr"));
            userControllerActor.Tell(new AddUser("Pawel"));

            Console.ReadLine();
            Console.WriteLine("Sending a message to ApplicationActor(Piotr)...");
            var actor1 = system.ActorSelection("/user/ApplicationUserControllerActor/Piotr");
            actor1.Tell("Testing actor 1");

            Console.ReadLine();
            Console.WriteLine("Sending a message to ApplicationActor(Pawel)...");
            var actor2 = system.ActorSelection("/user/ApplicationUserControllerActor/Pawel");
            actor2.Tell("Testing actor 2");

            Console.ReadLine();
            Console.WriteLine("Requesting the system shutdown.");
            system.Shutdown();
            system.AwaitTermination();

            Console.ReadLine();
        }
    }

Wyświetl pełny artykuł

Link do komentarza
Udostępnij na innych stronach

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Gość
Odpowiedz...

×   Wkleiłeś zawartość bez formatowania.   Usuń formatowanie

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Utwórz nowe...