Notki po polsku

Ncqrs

W ciągu ostatnich dwóch tygodni moją uwagę przykuł na dobre nowy framework open source — Ncqrs (witryna CodePlex Ncqrs znajduje się tutaj). Jak sugeruje nazwa, Ncqrs służy do budowy systemów w oparciu o wzorzec architektoniczny Command-Query Responsibility Separation (CQRS). To, czego nazwa nie mówi, to fakt, że Ncqrs narzuca pewną specyficzną implementację wspomnianego wzorca, a mianowicie tę opartą o technikę Event Sourcing. Na podstawie posta Grega Younga można by się czepiać, że nazwa Ncqrs nie jest zbyt trafna, ale odłóżmy kwestie nomenklatury na bok. Czym jest Ncqrs i jak to się stało, że mnie tak zafascynował?

Zasada działania

Ncqrs jest całościowym rozwiązaniem służącym do budowy systemów opartych o silny model domeny, którego stan jest przechowywany za pomocą strumienia zdarzeń. Poniższy diagram prezentuje workflow dla pojedynczego przypadku użycia w Ncqrs.

Komendy

Punktem wejścia do Ncqrs są komendy. Są to obiekty, który reprezentują żądania wykonania pewnej operacji na modelu domeny. Komendy są mapowane na operacje za pomocą rozbudowanego rozszerzalnego mechanizmu. Najprostsza implementacja mappera opiera się na dwóch atrybutach, które określają, czy dana komenda ma wykonywać metodę istniejącego obiektu (w nomenklaturze Ncqrs — korzenia agregatu), czy też tworzyć nowy obiekt. Properties komendy są mapowane (na podstawie nazwy) do parametrów wybranej metody lub konstruktora. Ostatecznie, (jeśli to konieczne) z magazynu danych podnoszony jest odpowiedni obiekt i wykonywana jest odpowiednia metoda (lub konstruktor).

Operacje biznesowe

Operacje i konstruktory obiektów są w Ncqrs wywoływane tylko za pośrednictwem komend. Ich jedynym zadaniem jest wykonanie logiki biznesowej. Nie mogą one bezpośrednio modyfikować stanu obiektu. Zamiast tego, dozwolonym mechanizm modyfikacji stanu jest zgłaszanie zdarzeń. Operacje biznesowe mogą także komunikować się ze światem zewnętrznym.

Stosowanie zdarzeń

Jak już wspomniałem na wstępie, zdarzenia są sposobem przechowywania stanu obiektów w Ncqrs. Operacja biznesowa może zgłosić jedno lub więcej zdarzeń. Dla każdego z nich framework wyszukuje odpowiedniej metody je przetwarzającej. Mechanizm ten jest oczywiście rozszerzalny, a out-of-the-box Ncqrs zapewnia dwa sposoby wiązania zdarzenia z metodą obiektu biznesowego przeznaczoną do jego obsługi: za pomocą konwencji oraz za pomocą atrybutów. Ncqrs wywołuje znalezioną metodę przekazując jej zgłoszone zdarzenie.

Przetwarzanie zdarzeń

Metoda przetwarzająca zdarzenie modyfikuje stan obiektu na podstawie danych przekazanych zdarzeniu. Tylko tyle i aż tyle. Metoda ta nie powinna zawierać żadnej logiki biznesowej (warunkowej), ani mieć jakichkolwiek skutków ubocznych (komunikacja z innymi systemami itp.).

Zapewne chcielibyście zapytać po co tyle komplikacji? Dlaczego operacja biznesowa nie może zmodyfikować stanu? Odpowiedź jest prosta. Ponieważ stan obiektów jest reprezentowany jako strumień zdarzeń przez nie wygenerowany, aby odtworzyć obiekt niezbędne jest stworzenie jego pustej instancji, a następnie przetworzenie (w kolejności!) wszystkich zapisanych zdarzeń — oczywiście za pomocą odpowiednich metod przetwarzających. Metody te są więc nie tylko stosowane do modyfikacji stanu podczas przetwarzania, ale także do odtwarzania tego stanu podczas podnoszenia obiektu z trwałego magazynu.

Dzięki takiemu podejściu systemy Event Sourcing (jak Ncqrs) zapewniają, za darmo, ślad audytowy, który ma gwarancję poprawności, ponieważ jest on wykorzystywany do budowy obiektów podczas normalnego działania systemu.

Publikowanie i denormalizacja zdarzeń

Jeśli wszystko do tej pory przebiegło prawidłowo, wszystkie zgłoszone zdarzenia są publikowane. Oczywiście, także w tym wypadku Ncqrs pozwala wymienić mechanizm publikacji zdarzeń. Domyślny wykorzystuje komunikację wewnątrz procesu, ale dostępny jest także taki, który wykorzystuje NServiceBus.

Publikowanie zdarzeń ma dwa cele. Po pierwsze, pozwala powiadomić zainteresowane systemy zewnętrzne o zmianach stanu naszego systemu. Polega to na eskalowaniu “lokalnych” (dotyczących naszego systemu) zdarzeń do statusu zdarzeń “globalnych” (mających znaczenia dla całego środowiska systemów). Stąd już tylko jeden krok do pełnej Event Driven Architecture (EDA).

Drugim celem publikowania zdarzeń jest tzw. denormalizacja, czyli aktualizacja podsystemu obsługi zapytań. Jaki podsystem? O co chodzi? Dokładny opis zagadnienia CQRS znajduej się tutaj. W tym miejscu wspomnę tylko, że systemy CQRS wykorzystują zwykle dwa osobne magazyny danych dla przetwarzania komend oraz do realizacji zapytań. Do synchronizacji tego drugiego magazynu danych wykorzystywane są właśnie denormalizatory zdarzeń. Proces denormalizacji polega na wykonaniu w bazie danych dla zapytań modyfikacji, które wynikają z opublikowanego zdarzenia. Skąd taka nazwa? Otóż zdarzenia stanowią znormalizowaną (pozbawioną redundancji) postać danych. W magazynie dla zapytań zaś, te same dane mogą mieć wiele reprezentacji, ponieważ nadrzędnym celem jest optymalizacja czasu realizacji zapytań.

Dlaczego to może działać?

Jest kilka powodów, które sprawiają, że (pozornie) szalona idea reprezentacji stanu obiektów jako ciągu zdarzeń może działać w praktyce. Oto kilka z nich:

  • zapewnia darmowy, gwarantowany, ślad audytowy
  • możliwość wykonywania tzw. snapshot’ów (czyli pełnych zrzutów zserializowanego obiektu biznesowego) co N zdarzeń. Dzięki temu odtwarzanie obiektu wymaga jedynie przetworzenia zdarzeń, które nastąpiły po ostatnim snapthot’cie
  • w klasycznym DDD (z użyciem O/RM) podczas podnoszenia obiektu z bazy danych tak naprawdę pobierane jest wiele wierszy danych (np. za pomocą podzapytań i złączeń). W przypadku Event Sourcingu pobieranych jest kilka wierszy przechowujących zdarzenia. Oba podejścia mają więc podobną złożoność na poziomie bazy danych.

Zapraszam Was do zabawy z Ncqrs. Postaram się odpowiedzieć na wszystkie Wasze pytania.

VN:F [1.8.7_1070]
Rating: 0.0/5 (0 votes cast)

Windows Communication Foundation 4.0

Druga część mojej sobotniej prezentacji dotyczyła nowości w Windows Communication Foundation. Aby zmieścić się w założonych piętnastu minutach, spośród wielu ciekawostek o których można poczytać np. u Nicholasa Allena, wybrałem dwie:

  • RoutingService
  • WS-Discovery

Dlaczego akurat te dwie? Otóż dlatego, że udało mi się wymyślić jeden wspólny temat, pod szyldem którego mógłbym je prezentować — usługę Event Brokera. Jeśli ktoś nie kojarzy tego wzorca, Event Broker to pośrednik, który forwarduje informacje o zdarzeniach pochodzące od nadawcy (źródła) do dowolnej liczby odbiorców. Celem tego wzorca jest zmniejszenie powiązania miedzy nadawcą i odbiorcami.

Projekt

Poniższa prezentacja (6 slajdów) pokazuje sposób działania Event Brokera. Pozostaje jedynie mapowanie poszczególnych funkcji na dostępne technologie. Dzięki nowemu WCF, wszystko czego potrzebujemy znajduje się już na naszym komputerze.

Funkcjonalność rejestracji subskrybentów (otrzymujących komunikaty) w Event Brokerze zapewnia nam obsługa WS-Discovery. Filtrowanie i routowanie przychodzących komunikatów do zainteresowanych odbiorców to funkcje nowego, wbudowanego w WCF, RoutingService’u. Wszystko, co musimy zrobić własnoręcznie to sprzęgnąć te dwie technologie tak, aby reakcja na ogłoszenia WS-Discovery powodowała odpowiednią modyfikację tablicy routingu.

Routing

RoutingService to gotowa do użycia usługa dostarczana wraz z nowym WCF.  Jej użycie wymaga trzech prostych kroków: stworzenia nowego hosta, skonfigurowania rodzaju routingu oraz podpięcia odpowiedniego zachowania. W kodzie wygląda to tak:

var routerServiceHost = new ServiceHost(typeof (RoutingService));
routerServiceHost.AddServiceEndpoint(typeof (ISimplexDatagramRouter), new BasicHttpBinding(), RouterAddress);
routerServiceHost.Description.Behaviors.Add(new RoutingBehavior(subscriptionManager.RoutingConfiguration));

Konfiguracja rodzaju routingu polega na utworzeniu endpoint’u odpowiedniego typu. Ja wykorzytsałem ISimplexDatagramRouter ponieważ potrzebuje przekazywać pakiety w jednym kierunku. Możliwy jest jednak także routing komunikacji request-response (z tym, że raczej już bez multicast’u).

Do konstruktora RoutingBehavior przekazujemy obiekt RoutingConfiguration, który m.in. zawiera tablicę routingu. Tablica ta może być modyfikowana z zewnątrz w dowolnym momencie, a dokonane zmiany zostaną uwzględnione podczas routowania następnego komunikatu. Kod modyfikujący tablicę wygląda np. tak:

var contract = ContractDescription.GetContract(typeof (ISimplexDatagramRouter));
var subscriber = new ServiceEndpoint(contract, new BasicHttpBinding(), address);
RoutingConfiguration.FilterTable.Add(new ActionMessageFilter(actions), new List<ServiceEndpoint> {subscriber});

WS-Discovery

Mój Event Broker wykorzystuje protokół WS-Discovery poprzez nasłuchiwanie na ogłoszenia (announcements) o pojawieniu się lub zniknięciu usług. Aby dla danej usługi WCF włączyć wysyłanie tych ogłoszeń, wystarczy zmodyfikować jej konfiguracje według poniższego wzorca:

<services>
  <service behaviorConfiguration="discoveryBehavior"
            name="Subscriber.SayHelloService">
    <endpoint name="udpDiscoveryEpt" kind="udpDiscoveryEndpoint" />
  </service>
</services>
<behaviors>
  <serviceBehaviors>
    <behavior name="discoveryBehavior">
      <serviceMetadata/>
      <serviceDiscovery>
        <announcementEndpoints>
          <endpoint kind="udpAnnouncementEndpoint" />
        </announcementEndpoints>
      </serviceDiscovery>
    </behavior>
  </serviceBehaviors>
</behaviors>

Konieczne jest dodanie nowego endpointu oraz dodanie do usługi odpowiedniego zachowania. W kodzie nie są potrzebne żadne zmiany. Modyfikacja ta sprawi, że subskrybenci będą wysyłać ogłoszenie w momencie przejścia w tryb on-line. Kolejnym krokiem jest dodanie do Event Brokera kodu, który będzie nasłuchiwał na te ogłoszenia i, zgodnie z ich treścią, modyfikował tablicę routingu. Funkcjonalność tą realizuje poniższy fragment:

var subscriptionListener = new SubscriptionListener(subscriptionManager);
var announcementService = new AnnouncementService();

announcementService.OnlineAnnouncementReceived += (s, a) => subscriptionListener.Subscribe(a);
announcementService.OfflineAnnouncementReceived += (s, a) => subscriptionListener.Unsubscribe(a);

var announcementServiceHost = new ServiceHost(announcementService);
announcementServiceHost.AddServiceEndpoint(new UdpAnnouncementEndpoint());

Jak widać, usługa WS-Discovery oferuje dwa eventy, pod które możemy podpiąć się z własnym kodem. Jest to bardzo wygodny sposób pracy.

Podsumowanie

Zapraszam do ściągnięcia całego przykładu z galerii MSDN. Mam nadzieję, że powyższy post pozwoli Wam łatwiej zrozumieć kod. Specjalnie nie omawiałem klasy obsługującej parsowanie WSDL-a, która jest niezbędna w ostatecznym rozwiązaniu, jednak nie jest ważna z punktu widzenia funkcjonalności Event Brokera. WCF 4 — polecam każdemu!

VN:F [1.8.7_1070]
Rating: 0.0/5 (0 votes cast)

Entity Framework 4 a NHibernate

W minioną sobotę miałem przyjemność uczestniczyć w krakowskiej edycji Visual Studio Community Launch. Co prawda, jako jeden z organizatorów, nie jestem najlepszą osobą do obiektywnej oceny tego wydarzenia, ale moim zdaniem było super.

Podczas VSCL miałem okazję sprawdzić się także w roli prelegenta, prowadząc dwie piętnastominutowe mikroprezentacje dotyczące Entity Framework 4 oraz Windows Communication Foundation 4. Przykłady kodu dla obu prezentacji umieściłem na MSDN Code Gallery odpowiednio tutaj i tutaj. Zapewne duża część z Was nie była na konferencji, dlatego postanowiłem owe przykłady omówić tutaj, na blogu. Dziś – EF4.

W obu przypadkach starałem się wpleść prezentację nowych funkcji danej technologii w jakąś większą historię. Dla Entity Framework tą większą historią jest możliwość współdzielenia (części) modelu z aplikacją napisaną w NHibernate.

Design

Solution wygląda tak:

Znajdują się w nim dwie aplikacje: obsługa zamówień (Orders) oraz bank (Accounts). Obie aplikacje składają się z prostego programu demonstrującego działanie, modelu domeny (Model) oraz obsługi persystencji (DataAccess). Współdzielony fragment to Parties – projekt niezależny od technologii persystencji (bez referencji do NHibernate lub EF) zawierający implementację podmiotów (osób fizycznych i organizacji).

POCO

Jednym z celów nowej wersji Entity Framework było wsparcie dla budowy modeli POCO (Plain Old CLR Object). Dlaczego jest to takie ważne? Ponieważ właśnie obsługa POCO pozwala wykorzystywać w EF klasy, które nie były napisane konkretnie z myślą o nim oraz, patrząc z drugiej strony, pozwala wykorzystać klasy napisane specjalnie dla EF w innych kontekstach. POCO daje nam komfort braku zależności od frameworku ORM na poziomie modelu domeny – coś, czego nie sposób przecenić. Dla przykładu, klasa Party reprezentująca podmiot wygląda tak:

public class Party
{
   public virtual int Id { get; protected set; }
   public virtual Address Address { get; protected set; }

   protected Party(Address address)
   {
      Address = address;
   }

   protected Party()
   {
   }
}

Lazy Loading

Leniwe ładowanie (lazy loading) jest kolejnym wielkim nieobecnym z pierwszej wersji EF, który doczekał się implementacji w wersji drugiej (czwartej). Jest ono jednak domyślnie wyłączone. Aby je włączyć, należy dodać do konstruktora kontekstu następującą linię:

ContextOptions.LazyLoadingEnabled = true;

Wszystko jednak ma swoją cenę. Ceną, którą płacimy za obsługę leniwego ładowania w EF jest konieczność „wirtualizacji” wszystkich property klas modelu. Jest to niezbędne, ponieważ EF w trakcie działania systemu generuje dynamicznie klasę dziedziczącą po naszej. Nie jest to jednak wielki problem, ponieważ NHibernate ma analogiczne wymaganie. Możemy więc spokojnie przywyknąć do myśli, że standardem dla ORM jest konieczność deklarowania wirtualnych property.

Value Objects

Value Objects są nazywane w EF „Complex Type”. Ot, taka fanaberia ludzi z Microsoft. Complex Type w EF może być modyfikowalny. Ja osobiście jednak odradzałbym to ze wszystkich. Niemodyfikowalność Value Object jest bardzo ważną cechą, m.in. ułatwiającą testowanie.

Ponieważ dla Complex Type EF nie prowadzi change tracking’u (sprawdzenie, czy należy zaktualizować bazę wykonywane jest za pomocą porównania wartości aktualnych i pobranych), nie ma sensu wirtualizacja properties CT. Znowu, jest to zgodne z tym, jak ja pracuje z NHibernate. Moje Value Objects muszą być jak najbardziej niezależne kontekstu bazodanowego. Jedyne na co się mogę (i niestety muszę) zgodzić to pusty konstruktor z widocznością protected. Zarówno EF, jaki NHibernate go wymagają. Efektem tego zestawu wymagań jest klasa Address będąca Value Objectem reprezentującym adres podmiotu:

public class Address
{
   public string Street { get; private set; }
   public string BuildingNumber { get; private set; }
   public string City { get; private set; }
   public string Country { get; private set; }

   protected Address()
   {
   }

   public Address(string street, string buildingNumber, string city, string country)
   {
      Street = street;
      BuildingNumber = buildingNumber;
      City = city;
      Country = country;
   }
}

Enkapsulacja

Enkapsulacja to zawsze dobra rzecz. Nie inaczej jest w wypadku modelu domeny. Upublicznianie getterów (a broń Boże setterów) właściwości jest prostą drogą do degradacji naszego modelu do roli prostych struktur danych. A przecież nie o to nam wszystkim chodzi. Z tego powodu bardzo się zmartwiłem po przeczytaniu na MSDN opisu wymagań dla POCO w EF. Wynika z niego (chyba, że ja źle rozumiem), że aby leniwe ładowanie działało, wszystkie właściwości klasy muszą być „public virtual”.

Na szczęście przeglądając sieć natknąłem się na wzmiankę, iż wystarczy „protected virtual”. Postanowiłem sprawdzić. Jakaż była moja radość, gdy okazało się, że wszystko działa. W kontekście enkapsulacji EF4 ma analogiczne wymagania, co NHibernate. Yupi:-)

Klasa Account jest świetnym przykładem enkapsulacji dla kolekcji: lista operacji związanych z kontem nie może być modyfikowana bezpośrednio, ale jedynie poprzez wywołania Credit lub Debit:

public class Account
{
   //...
   public decimal Balance { get; protected set; }
   protected virtual IList<Operation> Operations { get; set; }

   //...
   public virtual void Debit(decimal amount, string title)
   {
      if (Balance - amount < 0)
      {
         throw new InvalidOperationException();
      }
      var op = new Operation(-amount, title);
      Operations.Add(op);
      Balance -= amount;
   }

   public virtual void Credit(decimal amount, string title)
   {
      var op = new Operation(amount, title);
      Operations.Add(op);
      Balance += amount;
   }
}

Podsumowanie

Druga edycja Entity Framework jest o niebo lepsza od poprzedniej. Właściwie mogę powiedzieć z czystym sumieniem, że jest nawet używalna w kontekście Domain-Driven Design. Cieszy mnie to niezmiernie, bo lubię mieć wybór. Pluralizm to dobra rzecz. Czy rozważam przesiadkę z NHibernate na EF? Na pewno nie w tej wersji. Przewaga NH w przypadku bardziej skomplikowanych kwestii jest jeszcze zbyt duża. Z drugiej strony nie potrzebuję (i nie będę potrzebował) wszystkich tych kwestii związanych z obsługą architektur n-tier, w które Microsoft pakuje tyle pary.

Jeśli jednak EF będzie się rozwijać w tak szybkim tempie, jest wysoce prawdopodobne, że wersja 6.0 będzie stanowić groźną konkurencje dla NHibernate także w kontekście DDD.

VN:F [1.8.7_1070]
Rating: 0.0/5 (0 votes cast)

RavenDB – najprostsza baza danych, jaką widziałeś

RavenDB właśnie został oficjalnie opublikowany. Przez kilka ostatnich dni popołudniami i ł eksperymentowałem z tą technologią. Teraz chciałbym się z Wami podzielić moimi odczuciami.

Z początku byłem nastawiony bardzo sceptycznie — kolejna zabawka Ayende. Po Rhino DHT, Rhino PHT, Rhino Queues, Rhino ServiceBus i innych, których nie pamiętam, straciłem entuzjazm. W końcu jednak postanowiłem dać Raven’owi szanse. W końcu jest to jedyny produkt z kategorii NoSQL napisany w .NET.

Moje eksperymenty rozpocząłem od zabawy z klienckim API. Jest to coś, co wyróżnia ten produkt na tle konkurencji. Inne bazy dokumentowe albo wcale nie mają .NETowego API, albo jest ono napisane na odczepkę i po macoszemu. API klienckie Ravena jest przemyślane i dopracowane w każdej kwestii. Prawdopodobnie dlatego, że jest oparte o NHibernate. Przestawienie się z jednego na drugie było dla mnie kwestią minut: zamiast ISessionFactory — IDocumentStore, zamiast ISession — IDocumentSession.

Z naśladowaniem API NHibernate wiąże się jeszcze jedna unikatowa cecha API Ravena — implementacja wzorca Unit of Work. Oznacza to, że w ramach jednej instancji IDocumentSession każdy pobrany dokument ma dokładnie jedną reprezentację w pamięci. Jeśli pobierzemy dwukrotnie ten sam dokument, Raven automatycznie wykryje ten fakt i za drugim razem zwróci nam pobraną wcześniej instancję. Możemy więc śmiało używać operatora == na obiektach wyciągniętych z bazy.

Kolejnym miłym zaskoczeniem była dla mnie obsługa transakcji. Na poziomie protokołu HTTP Raven może przetwarzać wiele zleceń modyfikacji danych wysłanych za pomocą jednego żądania. Taka paczka jest wtedy przetwarzana transakcyjnie — albo wykonają się wszystkie modyfikacje, albo żadna. Na poziomie API klienta, Raven automatycznie łączy w paczki wszystkie modyfikacje z danej instancji IDocumentSession. Nasz unit of work jest więc także transakcją. Czy może być coś prostszego?

API Ravena wspiera także sharding, czyli technikę pozwalającą klientowi traktować wiele instancji bazy danych jako jedną wielką instancję. Co najważniejsze, wsparcie to jest zupełnie przeźroczyste dla kodu — zrealizowane jest po prostu jako inna implementacja pary interfejsów IDocumentStore oraz IDocumentSession.

Zachęcam Was wszystkich do poeksperymentowania z Ravenem. Ruch NoSQL zasługuje na odrobinę uwagi. Zrozumiałe jest dla mnie, że nie każdy od razu rzuca się na CouchDB i pisze “z palca” funkcje map-reduce w Erlangu, ale Raven to zupełnie inna para kaloszy. Raven jest napisany w .NET specjalnie dla developerów piszących w .NET! Naprawdę warto dać mu szansę.

VN:F [1.8.7_1070]
Rating: 5.0/5 (1 vote cast)

Sześć tygodni nietrywialnego modelu

Minęło właśnie sześć tygodni od opublikowania notki omawiającej szkic architektury systemu, nad którym właśnie pracuje. Tak, jak zakładaliśmy, wchodzimy właśnie z naszym systemem w fazę testów akceptacyjnych. Wdrożenie produkcyjne zbliża się wielkimi krokami. Przy tej okazji chciałbym Wam opowiedzieć, co zmieniło się w projekcie naszego systemu w ciągu tych pracowitych sześciu tygodni.

Architektura

Większość definicji architektury odnosi się do jej niezmienności — architektura, to ta część projektu, która jest kosztowna lub trudna do zmiany w późniejszych jego etapach. Ponieważ nie przechodziliśmy przez żaden bolesny proces przeprojektowywania, mogę z całą pewnością stwierdzić, że nasza architektura pozostała niezmieniona. Na początku projektu zdefiniowałem architekturę tak:

model zakłada istnienie anemicznych struktur danych, które są przetwarzane przez wiele następujących po sobie procesów. Każdy proces jest zaś sekwencją czynności, z których każda reaguje na konkretny rodzaj zdarzenia. Efektem wykonania czynności jest opublikowanie innego zdarzenia, aktywujące kolejną czynność.

Prawda, że minimalna ta definicja? Dobra architektura powinna być właśnie minimalna, ponieważ tylko wtedy może być stabilna. Jeśli architekt robi zbyt wiele założeń, jest więcej niż pewne, że któreś z tych założeń okaże się fałszywe w toku prac nad projektem.

Infrastruktura

Celem infrastruktury jest implementacja założeń architektury. W tym obszarze, jak w każdym innym, staram się kierować zasadą YAGNI (You Ain’t Gonna Need It). Z tego powodu infrastruktura w moim projekcie zmieniała się dosyć często.

Na początku było…

Na początku w ogóle jej nie było! Przez pierwszy tydzień lub dwa skupiliśmy się na rozwoju logiki systemu. Chcieliśmy mieć działający prototyp, który przechodzi testy “na sucho” (bez odwoływania się do bazy danych itp.). W ten sposób chcieliśmy udowodnić, że model procesowy w ogóle sprawdza się w przypadku naszego problemu. Gdyby okazało się inaczej, mielibyśmy jeszcze całkiem sporo czasu na zmianę założeń.

Pierwsza wersja

Ostatecznie przyszedł czas na pierwszą wersji infrastruktury. Napisanie jej zajęło mi jakieś 2 dni. Wersja ta obsługiwała wszystkie trzy tryby łączenia zdarzeń i czynności (natychmiastowy, z checkpointem i odroczony). Nie oferowała jedna żadnych mechanizmów kontroli stanu działania (ile zdarzeń oczekuje na przetworzenie itp.) ani historii wykonania (jakie czynności zostały wykonane w kontekście danego obiektu biznesowego). Była wystarczająca do wewnętrznych testów i o to nam chodziło.

Mechanizm był banalnie prosty. Każde zakolejkowane zdarzenie było adresowane do konkretnej docelowej czynności. W momencie publikacji odszukiwane były wszystkie zasubskrybowane czynności i do kolejki odkładane było tyle kopii zdarzenia, ile znaleziono subskrybentów. Dzięki temu różne czynności mogły subskybować to samo zdarzenie z różnym opóźnieniem.

Przetwarzanie zrealizowane było w formie pętli, która w każdym obrocie pobierała pierwsze zdarzenie z kolejki, odnajdywała jego adresata, przekazywała mu obiekt zdarzenia a następnie usuwała zdarzenie z kolejki oczekujących. Efektem takiego działania mogło być opublikowanie nowych zdarzeń. Te z nich, które były subskrybowane trwale (z checkpoint‘em) odkładane były do kolejki w bazie danych. Pozostałe kolejkowane były w pamięci do natychmiastowego przetworzenia w ramach tej samej transakcji.

Historia

Pierwszym dodatkowym wymaganiem, jakie się pojawiło, było odkładanie historii przetwarzania. “Nic prostszego!” — odpowiedziałem naszemu Product Owner‘owi. Dodanie kodu wiążącego zdarzenie z obiektem zajęło mi dosłownie chwilę. Efektem ubocznym tej poprawki była zmiana kodu usuwającego przetworzone zdarzenia na kod, który oznaczał je flagą “wykonane”. Jakie to miało konsekwencje dowiecie się niedługo.

Diagnostyka

Następna zmiana została zainicjowana przeze mnie. W pierwszej wersji, do zapisu danych związanych ze zdarzeniami, użyliśmy serializacji binarnej System.Runtime.Serialization. Dlaczego? Ponieważ ten mechanizm jest wspierany przez NHibernate out-of-the-box. Niestety ma on dwie wady. Po pierwsze, rozmiar danych po serializacji jest dosyć duży. Po drugie (i gorsze), dane te są zupełnie nieczytelne, zarówno z poziomu SQL Server Management Studio, jak technologii GUI, którą zastosowaliśmy (własne rozwiązanie 4GL). Przejście na serializację JSON (z formatowaniem) rozwiązało problem diagnostyki (wykorzystaliśmy bardzo dobrą bibliotekę JSON.NET).

Wydajność

Przetwarzanie zdarzeń oparte było o jedną tabelę w bazie danych. Zdarzenia, która są gotowe do przetworzenia wyciągane były mniej-więcej takim zapytaniem:

SELECT * FROM QueuedEvent WHERE DueDate <= @currentTime AND Processed = 0

Ten drugi warunek został dodany w momencie implementacji obsługi historii. Niestety wydajność takich zapytań, wziąwszy pod uwagę ilość zdarzeń, jakie mogą się odłożyć po roku od uruchomienia systemu, nie byłaby zapewne zadowalająca. Pierwszym rozwiązaniem, jakie się nasuwa, jest partycjonowanie. Niestety w tym wypadku zdefiniowanie funkcji partycjonującej byłoby trudne. Postanowiliśmy więc rozdzielić zdarzenia na dwie osobne tabele: jedną dla zdarzeń oczekujących i jedną dla przetworzonych. Ta pierwsza zwykle będzie zawierać nie więcej niż kilkadziesiąt rekordów, więc nie ma sensu w ogóle jej indeksować. Dzięki temu częste wstawienia i usunięcia będą tańsze.

Wydajność raz jeszcze

W końcu przyszedł czas na podejście do problemu zrównoleglenia przetwarzania. Okazało się, że czas odpowiedzi zewnętrznych systemów, z którymi się integrujemy, jest bardzo długi. Miało to bardzo niekorzystny wpływ na przepustowość naszego systemu, gdyż w podczas odczekiwania na odpowiedź dla jednego zdarzenia, inne nie były przetwarzane.

Rozwiązanie jest proste — zrównoleglijmy przetwarzanie. Prościej powiedzieć niż zrobić. Ostatecznie jednak się udało. Nasze rozwiązanie składa się z podręcznej pamięci przechowującej identyfikatory zdarzeń oczekujących na przetwarzanie i wielu wątków pobierających kolejne identyfikatory. Cache automatycznie odświeża się, gdy zostanie opróżniony.

Aby nie blokować całej tabeli ze zdarzeniami w momencie pobierania danych do cache, stosujemy tam najsłabszy poziom izolacji transakcji. Konsekwencją tego jest fakt, że nie jesteśmy pewni, czy zdarzenia, których identyfikatory pobraliśmy, są jeszcze zdatne do przetworzenia. Fakt ten jest weryfikowany przed samym przetworzeniem oraz po jego zakończeniu (optimistic concurrency).

Morał

Każda historia ma swój morał. W tym wypadku chodzi o relację architektura — design. Architektura w tym projekcie była stabilna. Z drugiej strony design (czyli głównie infrastruktura) zmieniał się praktycznie cały czas.

Stabilność architektury (zaklętej w interfejsach) pozwoliła zbudować szybko całą logikę biznesową aplikacji, bez potrzeby robienia kosztownych restrukturyzacji po każdej modyfikacji infrastruktury.

Najlepsza architektura to taka, której prawie nie widać. Najlepszy design to taki, który rozwija się wraz z projektem i jego potrzebami.

VN:F [1.8.7_1070]
Rating: 2.7/5 (3 votes cast)