AKKA.NET – obsługa błędów

W poprzednich postach o AKKA.NET pisałem o hierarchii aktorów. Stanowi ona bardzo ważny element systemów opartych na aktorach. Zasada jest taka, że uproszczamy problem na podproblmy, aż do momentu, gdy każdy podproblem jest łatwy w implementacji za pomocą pojedynczego aktora.  Dla przypomnienia, stworzyliśmy następującą hierarchię:

controller

Co się stanie, gdy jeden z aktorów wyrzuci wyjątek? AKKA.NET posiada kilka mechanizmów. Przede wszystkim jednak, taki aktor zostanie wstrzymany, możliwe  wraz ze wszystkimi potomkami (w zależności od przyjętej strategii). Następnie do rodzica zależy, co należy zrobić. Możliwe strategie to:

  • Węzeł podrzędny zostanie wznowiony
  • Węzeł podręczny zostanie zatrzymany
  • Węzeł podręczny zostanie zrestartowany
  • Rodzic może nie wiedzieć jak obsłużyć dany błąd. Z tego względu jest możliwość eskalacji problemu do kolejnego rodzica (czyli dziadka względem węzła, który wywołał problem).

Dobór odpowiedniej strategi jest dość trudny i należy wiedzieć czym się charakteryzuje każda z nich.

W przypadku wznowienia działania, stan wewnętrzny aktora jest utrzymany. Ponadto wszystkie dzieci danego węzła również zostaną wznowione.

Dla odmiany restart aktora powoduje usunięcie całego stanu.  Jeśli wiemy, że stan aktora jest błędny (niespójny), wtedy taka strategia jest rozsądna. Analogicznie potomkowie również zostaną zrestartowani.

Przez stan wewnętrzny mam na myśli prywatne pola i zmienne. Kolejka z wiadomościami asynchronicznymi jest przechowywana gdzieś indziej. Jeśli zatem wiadomość “A” wywołała problem i zrestartowaliśmy aktora, wtedy kolejne wiadomości  np. “B”, które były już w kolejce, nie zostaną utracone. Pominiemy wyłącznie wiadomość, która wywołała błąd.

Analogicznie sytuacja wygląda z zatrzymaniem aktora. Jest to skrajna sytuacja, kiedy nie wiemy jak naprawić problem. Wtedy najbezpieczniej jest po prostu zatrzymać węzeł powodujący problem, wraz ze wszystkimi jego potomkami.

Zasadę eskalacji błędu do rodzica, myślę, że można już wywnioskować ponieważ wygląda analogicznie. Węzeł jest wstrzymany tymczasowo w momencie przekazania kontroli do kolejnego rodzica.

Implementacja obsługi błędów w AKKA.NET sprowadza się do tzw.  “Supervision strategy”.

Pierwsza, domyślna strategia to OneForOneStrategy. Oznacza to, że akacja zostanie podjęta wyłącznie na węźle, który spowodował problem. Załóżmy, że mamy rodzica P z dziećmi A, B, C. Jeśli wyjątek został wyrzucony przez “A”, wszelkie akcje (takie jak zatrzymanie, restart) zostaną podjęte na tylko A.

AllForOneStrategy z kolei podejmie akcje dla każdego z rodzeństwa. Oznacza to, że jeśli A wywoła wyjątek to również B,C zostaną np. wstrzymane lub zrestartowane.

Powyższe strategie również definiują jaki wyjątek jaką akcję powinien spowodować (stop, restart, escalate, resume).

W praktyce wygląda to tak, że aktor zwraca swoją strategie, a strategia z kolei przyjmuje kilka parametrów określających możliwe akcje w zależności od wyjątku, liczby powtórzeń itp. W przyszłym wpisie, pokaże jak to wygląda od strony C#.

HSTS w ASP.NET z użyciem biblioteki NWebsec

W poprzednim poście opisałem zasadę działania protokołu HTTP Strict Transport Security. W skrócie najważniejsze punkty to:

  • Serwer zwraca specjalny nagłówek “Strict-Transport-Security”, który powinien być przesyłany wyłącznie przez HTTPS.
  • Po otrzymaniu nagłówka od serwera, przeglądarka zawsze będzie łączyć się przez HTTPS, a nie HTTP. Użytkownik jeśli nawet będzie chciał użyć HTTP, przeglądarka dokona wewnętrznego przekierowania na HTTPS (307 – internal redirect).

Oczywiście można samemu zwracać odpowiedni nagłówek, ale lepiej skorzystać z gotowych pakietów nuget. Często programiści zawsze zwracają strict-transport-security, co jest również błędne – specyfikacja mówi wyraźnie, że powinno go zwracać się wyłącznie przez https.

Skorzystamy zatem z pakietu NWebsec.  Zestaw zawiera wiele różnych bibliotek związanych z bezpieczeństwem aplikacji webowych. W naszym przypadku zainteresowani jesteśmy wyłącznie HSTS.

Po zainstalowaniu,  wystarczy w OWIN startup:

    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.UseHsts(options => options.MaxAge(seconds: 10886400).IncludeSubdomains().Preload());
        }
    }

Za pomocą FluentInterface możemy łatwo zmodyfikować każdy parametr nagłówka. Jeśli odpalimy teraz aplikacje, zobaczymy, że faktycznie nagłówek jest zwracany:

1

Warto jednak wciąż wymuszać globalnie HTTPS za pomocą atrybutu RequiresHttpsAttribute:

    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
            filters.Add(new RequireHttpsAttribute());
        }
    }

Ważne to jest dla pierwszego odczytania HSTS oraz stanowi to drugą warstwę bezpieczeństwa (np. gdyby dana przeglądarka nie wspierała HSTS).

Bezpieczeństwo web: Nagłówek HTTP Strict Transport Security (HSTS)

Protokół HTTPS jest dzisiaj powszechny na wszystkich stronach z wrażliwymi informacjami. Banki są klasycznym przykładem. HTTPS “gwarantuje”, że dane są przesyłane w szyfrowanej formie, a klient wie, że łączy się z oryginalną stroną. Certyfikat publikowany przez stronę jest gwarantem, że korzystamy właśnie z tej aplikacji, z której zamierzaliśmy. W najprostszej postaci wygląda to zatem następująco:

1

Tego przynajmniej spodziewamy się… Problem w tym, że czasami użytkownicy wpisują adres http://… W takich sytuacjach,  programiści przekierowują użytkownika na HTTPS, tzn. sekwencja wygląda następująco:

  1. Użytkownik wpisuje adres http://www.domain.com
  2. Serwer zwraca kod 302 – przekierowanie na https://www.domain.com
  3. Przeglądarka od teraz korzysta z bezpiecznej wersji czyli https://www.domain.com

Wygląda więc to następująco:

2

Niestety jest to bardzo niebezpieczna architektura. Bardzo dużo stron, włączając w to banki, korzysta z przekierowania na HTTPS.  Wiele banków korzysta z HTTP na stronie głównej, a do HTTPS jest użytkownik przełączony dopiero w momencie chęci zalogowania do strony. Oznacza to, że strona główna banku jest niebezpieczna.  Wszystko co na niej znajduje się może zostać podrobione (brak certyfikatu). Jeśli ktoś podmieni stronę główną i wstawi np. link do logowania do kompletnie innej strony,  możliwe, że staniemy się ofiarą phishing.

Wyobraźmy sobie, że korzystamy z Internetu w miejscu publicznym, np. używając WIFI. Możliwe, że ktoś wstawi proxy między nami, a docelowymi stronami.  Co jeśli staniemy się ofiarą ataku man in the middle? Taki serwer proxy będzie w stanie przekierowywać wszystkie nasze żądania. Wtedy powyższa sekwencja może wyglądać już tak:

  1. Użytkownik wpisuje http://www.domain.com
  2. Man in the middle przechwytuje żądanie i zwraca jakąkolwiek treść..
  3. Użytkownik nie zauważa nic podejrzanego. Strona wygląda identycznie – jesteśmy ofiarami phishing.

Czujny użytkownik zauważy, że po kliknięciu loguj na stronie głównej banku nie został przekierowany do https i wciąż korzysta z nieszyfrowanego protokołu. Niestety jest to rzadkość.

Problem jest jeszcze większy, ponieważ MITM (man in the middle), może rzeczywiście logować się do banku. Użytkownik poda hasło poprzez HTTP do MITM, a potem MITM nawiąże połączenie szyfrowane HTTPS z prawdziwą stroną banku. Możliwe jest zatem, że użytkownik będzie widział prawdziwe dane (balans konta itp), ale wszystko serwowane będzie przez MITM, który łączy się poprzez HTTPS z prawdziwym bankiem, a potem przekierowuje wszystko w postaci HTTP do użytkownika.

Rozwiązanie składa się z dwóch etapów. Przede wszystkim każdy bank powinien w pełni implementować HTTPS – na każdej podstronie.

Druga kwestia jest trudniejsza. Użytkownicy wciąż domyślnie będą wpisywać www.domain.com, a nie https://www.domain.com. Jak już wiemy, przekierowanie jest niebezpieczne bo nie wiadomo, czy ktoś po drodze nie przechwyci zapytania i nie będzie “karmił” nas dowolnymi danymi.

Z tego względu, wymyślono protokół HSTS (HTTP Strict Transport Security). Jest to protokół implementowany bezpośrednio przez przeglądarki. Jeśli serwer ustawi odpowiedni nagłówek, wtedy przeglądarka jest zobowiązana łączyć się zawsze przez HTTPS, a nie HTTP. Dzięki temu, MITM jest nie możliwy, ponieważ nigdy nie dojdzie do połączenia HTTP, a jak wiemy, dane przesyłane przez HTTPS nie mogą być zmodyfikowane.

Aby przeglądarka łączyła się zawsze przez https, serwer strony musi zwrócić następujący nagłówek w odpowiedzi:

Strict-Transport-Security:max-age=631138520; includeSubDomains; preload

Parametr max-age oznacza, jak długo przeglądarka ma łączyć się zawsze przez https. Powyższy nagłówek zawsze należy zwracać przez HTTPS, a nie przez HTTP. Wynika to z prostego faktu, że przeglądarka nie może ufać niczemu, co pochodzi z HTTP. Łatwo wyobrazić sobie, że przez HTTP, MITM modyfikuje np. max-age do 1.

Sekwencja może zatem teraz wyglądać następująco:
1. Użytkownik wpisuje http://www.domain.com
2. Serwer zwraca 302 oraz https://www.domain.com
3. Przeglądarka łączy się z https://www.domain.com
4. Serwer zwraca nagłówek Strict-transport-security.
5. Użytkownik wpisuje http://www.domain.com
6. Przeglądarka automatycznie przekierowuje na https://www.domain.com (kod 307, internal redirect). Od teraz MITM nie jest w stanie nam zaszkodzić, ponieważ nie wychodzi żadne połączenie HTTP.

Powyższe rozwiązanie ma jednak wciąż pewną lukę.Pierwsze połączenie HTTP wciąż jest niebezpieczne. Co prawda minimalizuje to groźbę ataku MITM ponieważ wystarczy, że użytkownik chociaż raz wcześniej korzystał ze strony (tak, że HSTS został ustawiony). Niestety w skrajnej sytuacji, wciąż mamy ten sam problem (MITM i pierwsze zapytanie z danej przeglądarki).

Z tego względu mamy tzw. pre-loaded lists. Przeglądarki korzystają z tych list i wtedy każda strona, która na niej znajduje się, będzie ładowana automatycznie wyłącznie przez HTTPS. Możemy naszą stronę dodać tutaj.
Po dodaniu do listy, przeglądarki będą łączyć się automatycznie przez HTTPS, nawet za pierwszym razem.
Możemy również zobaczyć adresy aktualnie dodanych stron do tych list.

W następnym wpisie, pokażę jak zaimplementować HSTS w ASP.NET MVC. W międzyczasie zachęcam zapoznać się z kompatybilnością różnych przeglądarek z HSTS .

Resharper: Postfix templates

Postfix templates to kolejne usprawnienie, dzięki któremu nie musimy korzystać co chwilę z myszki i skakać między liniami kodu. Postfix templates mają za zadanie uniknięcie cofania się kursorem do początku linii. Na przykład, jeśli definiujemy warunek, wtedy jeśli chcemy dodać IF, musimy cofnąć się. Podobnie z pętlami, using, lock i innymi konstrukcjami. Postfix oznacza, że szablon aplikujemy na końcu linii.

Postfix templates zostało zintegrowane z Resharper 10.0. Mój ulubiony szablon to “.var”. Bardzo często deklarujemy zmienne w następujący sposób:

var stringBuilder = new StringBuilder();

Za pomocą szablonów możemy napisać “StringBuilder.var” i zostanie wygenerowany powyższy kod. Ponadto, Intellitrace podpowie nam StringBuilder więc tak naprawdę będziemy musieli napisać coś w stylu “Str.var”. Screenshot:

1

Po naciśnięciu enter, zostanie wygenerowany kod:

2

Oczywiście trochę to zajmie czasu, aby zapamiętać wszystkie skróty szablonów oraz przyzwyczaić się do nich.  Na szczęście są one na tyle wygodne, że po pewnym czasie stają się tak naturalne, jak “alt+enter”.  Jeśli chcemy wyrzucić wyjątkiem wtedy wpisujemy:

3

Szablon wygeneruje oczywiście:

threw new NotImplementException();

Proszę zwrócić uwagę, że pierwsza cześć jest zawsze silnie typowana. Oznacza to, że w praktyce wystarczy, że naciśniemy kilka klawiszy i wyjątek zostanie wyrzucony (szczególnie, że nie ma spacji między nazwą wyjątku, a .throw). Wsparcie intellitrace jest jeszcze bardziej przydatne.

Kolejny przydatny szablon to .if. Najpierw piszemy warunek, np. a>5, a potem za pomocą .if wygenerujemy instrukcję warunkową:
4

Do dyspozycji mamy również różne typy pętli. Szczególnie interesującą jest .forr, czyli for iterujący od końca do początku tablicy:

5

Wygenerowany kod:

          for (var i = data.Length - 1; i >= 0; i--)
          {
              
          }

Z ciekawszych skrótów jest jeszcze: .null,.notnull czy .switch.

Pełna lista szablonów znajduje się tutaj. Nie chcę tutaj opisać każdej instrukcji osobno bo najlepiej po prostu popraktykować to samemu – na początku jest trudno przyzwyczaić się.

ASP.NET MVC: Wywoływanie funkcji JavaScript z parametrami z modelu

Czasami w widoku wywołujemy funkcję JavaScript z parametrami, które są przekazane za pomocą ViewModel z kontrolera. Załóżmy, że nasza metoda w kontrolerze wygląda następująco:

       public ActionResult Index()
       {
            return View(new FooModel("Hello World!"));
        }

ViewModel z kolei zawiera jedną właściwość:

    public class FooModel
    {
        public string Text { get; set; }

        public FooModel(string text)
        {
            Text = text;
        }
    }

Następnie zdefiniujmy jakąkolwiek funkcję JS przyjmującą parametry:

<script type="text/javascript">
    function doSomethingInJs(text) {
        $('#sampleContainer').html(text);
    }
</script>

Błędne wywołanie funkcji w razor wygląda z kolei następująco:

 
   <script type="text/javascript">
        doSomethingInJs('@Model.Text');
    </script>

Jest to nic innego, jak klasyczny przykład wstrzyknięcia złośliwego kodu. Dotyczy to wszystkich pól typu string. Nawet jeśli spodziewamy się tylko imienia czy nazwiska, nigdy nie wiadomo jakie dane zawiera view model. Zwykle dane pochodzą z bazy danych. Co jeśli w jakiś sposób, baza danych zawiera już złośliwy kod? Aplikacja powinna traktować wszystkie dane, jako potencjalne zagrożenie.

Co jeśli, następująca treść zostanie przekazana?

    public ActionResult Index()
    {
            return View(new FooModel("<script>alert('Hello World')</script>"));
    }

Na szczęście powyższy kod jest bezpieczny, ponieważ Razor zamieni <script> na znaczniki HTML:

    <script type="text/javascript">
        $(function() {
           doSomethingInJs('&lt;script&gt;alert(&#39;Hello World&#39;)&lt;/script&gt;');
        });
    </script>

Innymi słowy, kod zostanie wyświetlony jak zwykły tekst. Za każdym razem, jak wywołujemy @Model, zawartość jest enkodowana. Co jednak stanie się, jeśli treść zostanie przekazana jako ASCII HEX?

public ActionResult Index()
{
   var text =       "\\x3c\\x73\\x63\\x72\\x69\\x70\\x74\\x3e\\x61\\x6c\\x65\\x72\\x74\\x28\\x27\\x48\\x65\\x6c\\x6c\\x6f\\x20\\x57\\x6f\\x72\\x6c\\x64\\x27\\x29\\x3c\\x2f\\x73\\x63\\x72\\x69\\x70\\x74\\x3e";

   return View(new FooModel(text));
}

Razor nie zakoduje treści ponieważ jest to zwykły tekst (brak znaczników HTML). Javascript z kolei, rozpoznaje ASCII w postaci hex (za pomocą \x). Efekt będzie taki, że kod javascript wykona się i wyświetli alert. W tej chwili, jeśli zajrzymy do źródła strony, zobaczymy:

 <script type="text/javascript">
        $(function() {
           doSomethingInJs('\x3c\x73\x63\x72\x69\x70\x74\x3e\x61\x6c\x65\x72\x74\x28\x27\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64\x27\x29\x3c\x2f\x73\x63\x72\x69\x70\x74\x3e');
        });
  </script>

Rozwiązaniem jest endkodowanie parametrów funkcji JavaScript za pomocą HttpUtility.JavaScriptStringEncode:

    <script type="text/javascript">
        $(function() {
           doSomethingInJs('@HttpUtility.JavaScriptStringEncode(Model.Text)');
        });
    </script>

Efektem będzie:

    <script type="text/javascript">
        $(function() {           
doSomethingInJs('\\x3c\\x73\\x63\\x72\\x69\\x70\\x74\\x3e\\x61\\x6c\\x65\\x72\\x74\\x28\\x27\\x48\\x65\\x6c\\x6c\\x6f\\x20\\x57\\x6f\\x72\\x6c\\x64\\x27\\x29\\x3c\\x2f\\x73\\x63\\x72\\x69\\x70\\x74\\x3e');
        });
    </script>

Widzimy, że każdy kod ascii, zostały poprzedzony dodatkowym znakiem ‘\’.

Generalnie połączenie jQuery + mieszanie C# z JavaScript jest nie tylko złą praktyką z punktu widzenia bezpieczeństwa, ale również nie jest to zbyt czysty kod. Nie powinno mieszać się kodu inline z szablonami razor. Te dwie warstwy powinny rozwijać się niezależnie. Da nam to nie tylko łatwiejszy w utrzymaniu kod, ale również jak widać powyżej, łatwiej będzie uniknąć luk w bezpieczeństwie.

AKKA .NET – definiowanie ścieżki aktora

W poprzednim poście użyliśmy metody ActorSelection w celu uzyskania referencji do aktora:

var actor1 = system.ActorSelection("/user/ApplicationUserControllerActor/Piotr")

Dzisiaj chciałbym bardziej skupić się na definiowaniu ścieżki do aktora. Pełna ścieżka może zawierać następujące elementy:
– protokół
– nazwa systemu
– adres ip aktora
– seria nazw aktorów opisująca hierarchie np. ApplicationUserControllerActor/actor1/actor2 itp.

Załóżmy, że nasz aktor znajduje się na innym komputerze. Oznacza to, aby wysłać jakieś wiadomości do niego musimy skorzystać z protokołu sieciowego. Nazwa ścieżki będzie wtedy wyglądać:

akka.tcp://FooHierarchySystem@89.124.43.14/user/ApplicationUserControllerActor/Piotr

Jeśli nie korzystamy ze zdalnych aktorów, wtedy moglibyśmy napisać:

akka://FooHierarchySystem/user/ApplicationUserControllerActor/Piotr

W poprzednim poście jednak nie musieliśmy podawać nazwy systemu (FooHierarchySystem). Wynika to z faktu, że akka .net wspiera ścieżki relatywne. ActorSlection wywoływaliśmy bezpośrednio na systemie, zatem nie było potrzeby precyzowania tego w ścieżce. Można to porównać do ścieżek relatywnych występujących w systemie plików.

Załóżmy, że jesteśmy teraz w aktorze ApplicationUserControllerActor i hierarchia jest taka sama jak w poprzednim wpisie:
controller

W celu uzyskania dostępu do jednej z instancji ApplicationUserActor możemy:

// Pierwsze podejście
Context.ActorSelection("akka://FooHierarchySystem/user/ApplicationUserControllerActor/Piotr");
// Drugie podejście
Context.ActorSelection("user/ApplicationUserControllerActor/Piotr");
// Trzecie podejście
Context.ActorSelection("Piotr");

Wszystkie ścieżki wskazują na tego samego aktora, a dwie ostatnie są relatywne. Podobnie z ApplicationUserControllerActor możemy:

Context.ActorSelection("../../ApplicationUserControllerActor").Tell(new AddUser("AnotherUser"));

Tak jak w ścieżkach systemu plików, ../ oznacza wejście o jeden poziom do góry. W powyższym przykładzie ../../ da nam referencję na /user, a potem schodzimy z powrotem na ApplicationUserControllerActor.

AKKA.NET: Hierarchia aktorów

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

AKKA.NET – czas życia aktorów, zdarzenia (hooks)

Dzisiaj zacząłem pisać post o hierarchii aktorów. Jest to bardzo ważny element w celu osiągnięcia skalowalności i dobrej obsługi błędów (np. poprzez izolacje wadliwych aktorów).

W połowie jednak stwierdziłem, że najpierw wypada napisać krótki wpis o zdarzeniach (hooks), jakie możemy zdefiniować w AKKA. Pozwoli nam to potem lepiej zrozumieć przepływ informacji w hierarchiach aktorów.

Każdy aktor,  może znajdować się w następujących etapach:

  • Starting – aktor został dopiero stworzony i nie przetwarza jeszcze wiadomości
  • Receiving – aktor może teraz otrzymywać wiadomości
  • Stopping – zwalnianie zasobów
  • Terminated – aktor nie może już otrzymywać wiadomości, ponadto w tym stanie, nie może zostać już wznowiony.
  • Restarting – aktor aktualnie jest resetowany. Oznacza to, że po restarcie może przejść do “Starting”, a potem do “Receiving”, czyli będzie w stanie ponownie przetwarzać wiadomości.

Z perspektywy kodu, poszczególne stany można obserwować, przeciążając odpowiednie metody:

  • OnStart  (Starting) – metoda wywołana przed rozpoczęciem otrzymywania jakichkolwiek wiadomości
  • PostStop (Stopping) – zwalnianie zasobów
  • PreRestart – przed rozpoczęciem restartu.
  • PostRestart po zakończeniu restartu, ale jeszcze przed ponownym wywołaniem OnStart.

Kod:

 class FooActor : UntypedActor
    {
        protected override void OnReceive(object message)
        {
            Console.WriteLine("Received: {0}", message);
        }

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

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

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

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

W celu przetestowania możemy:

            var system = ActorSystem.Create("system");
            var actor = system.ActorOf<FooActor>();

            actor.Tell("Hello World!");
            system.Shutdown();
            system.AwaitTermination();

Poszczególne metody, przydadzą się w następnym wpisie, poświęconym hierarchii aktorów oraz relacjami między aktorami. Każdy z aktorów może być zarządcą więc śledzenie czasu życia aktorów jest pomocne w zrozumieniu tych zasad.