środa, 5 października 2011

Persystentny Multi-Listener, czyli... Saga

Persystentny multi-listener... jak to pięknie brzmi...:)

Cóż to takiego?

Listener - czy obiekt słuchający zdarzeń,
mulit - czyli chodzi o wiele zdarzeń,
persystentny - czyli posiadający trwały stan.


Podczas ostatnich warsztatów z zakresu projektu DDD-CqRS Leaven wspomniana wyżej technika modelowania czasu spotkała się z zainteresowaniem wielu osób. Nic dziwnego, jest to dosyć nowy pattern; również na szkoleniach wzbudza zawsze największe emocje. Głównie dlatego, że ciężko pogodzić się z myślą, że coś tak złożonego można modelować i implementować w tak prosty i przyjemny sposób:P

Pattern ma swoją oficjalną nazwę, brzmi ona Saga. Jeżeli saga kojarzy się senior developerom z sagą rodziny Carringtonów, to dobrze się Wam kojarzy. Saga to coś co może rozciągać się w czasie niczym telenowela. Saga jest patternem służącym do orkiestracji wielu zdarzeń, które mogą zajść w rozproszonym systemie w nieokreślonej kolejności. Czas pomiędzy zajściem zdarzeń może być relatywnie długi, dlatego należy persystować jej stan w czasie oczekiwania na kolejne zdarzenie.

Przykład:
Wyobraźmy sobie system, w którym mamy moduły: sprzedaży, płatności i magazynu.
W module sprzedaży możemy zatwierdzić obiekt biznesowy Zamówienie. Zamówienie rzuca wówczas zdarzenie biznesowe ZatwierdzonoMnie zawierające id tegoż zamówienia.
W module płatności możemy dokonać wpłaty. Być może jest to przedpłata dokonana wcześniej niż zatwierdzenie zamówienia... Moduł ten rzuca zdarzenie niosące informację o zajściu faktu dokonania wpłaty.
Oba zdarzenia są orkiestrowane przez sagę Zakupy. Jeżeli otrzyma ona oba zdarzenia (pasujące po id biznesowym) i stwierdzi, że jesteśmy "kwita" wówczas może wysłać sygnał do modułu magazynowego aby przygotować paczkę do wysyłki.

Oczywiście możemy tego typu proces zaimplementować w inny sposób niż zdarzeniowy, ale jeżeli zależy nam na decouplingu modułów systemu (wraz z wszystkimi konsekwencjami: skalowanie, testowanie, rozszerzalność, otwartość na dodawanie pluginów, redukcja architektury Speaghetti na poziomie modułów) to warto posłużyć się Sagą.


Udi Dahan dosyć dobrze wyjaśnia motywację oraz szczegóły koncepcyjne: Saga Persistence and Event-Driven Architectures dlatego nie chcę powtarzać, tego co zostało napisane ponad 2 lata temu.

Jak widać w powyższym linku technicznie Saga to złożenie 2 wzorców: Observer i Memento. O ile observera każdy zna choćby z bibliotek graficznych i wszechobecnych tam onClickListenerów, o tyle Memento jest mniej znanym patternem. Jeżeli ktoś oglądał film pt. Memento o człowieku, który miał problem z transferem danych z pamięci podręcznej (kora przed-czołowa) do pamięci długotrwałej (hipokamp) to skojarzenie jest również poprawne. Memento to pattern służący obiektom na wysyłanie do siebie informacji na przyszłość. Tak jak bohater filmu, który zapisywał i tatuował sobie na rękach informacje o tym co należy zrobić w przyszłości:)

Natomiast zainteresowanych szczegółami implementacji Sagi odsyłam do przykładu w projekcie DDD&CqRS Leaven:



//=============================================

Jako ciekawostkę dodam, że niektóre szyny komunikatów w .NET wspierają mechanizm Sagi niejako "z pudełka"...
Techniczny komunikat (message) jest traktowany na poziomie logicznym jak zdarzenie (event). Ichnie "Message Driven Beany" reagaujące na wiele zdarzeń to po prostu Sagi. Do tego automatyczne ładowanie i zapisywanie Memento, kończenie albo usypianie sagi oraz drobny szczegół: obsługa współbieżności...
Natomiast my w swym świecie Javy wciąż szukamy uber-frameworka webowego, który daje złudzenie, że każdy system jest CRUDem...

19 komentarzy:

ck pisze...

"ZatwierzonoMnie" - literóweczka :)

Sławek Sobótka pisze...

Dzięki, pewnie nie jedyna, ale ja posługując się dźwiękowym modelem mowy po prostu ich nie widzę:)

Wiem, że wzrokowcom ciężko to sobie wyobrazić, ale zachęcam do zgłębiania tematu, bo jest ciekawy...

ToJaJarek pisze...

(ja też mam "dźwiękowy model mowy ;))

wracając do wzorca: nie raz mam dylemat czy "transakcje" np. sprzedaży kojarzyć z klasa odpowiedzialną za przypadek użycia "Obsługa czegoś tam" czy raczej tworzyć świadczący wewnątrz systemu taką usługę dla pozostałych?

Wyjaśnienie: Osobiście traktuję przypadki użycia jako usługi systemu dla aktora, wiec w moich projektach "dodaj zamówienie", "zmień zamówienie", "lista zamówień" itp. to jeden use case "zarządzanie zamówieniami" z odrębnymi scenariuszami (i np. wzorzec strategii), pozwala to na hermetyzację pewnego aspektu pracy i nie mnożenie use cesów, które tak na prawdę operują na tym samym elemencie dziedziny...

Paweł Kaczor pisze...

Nazwa 'Persystentny Multi-Listener' nie koniecznie oddaje istotę Sagi, bowiem Saga może być bezstanowa. Bezstanowość osiąga się poprzez umieszczanie wszystkich informacji w komunikatach (szczególnie jeśli komunikat reprezentowany jest przez dokument). Generalnie Saga powinna tylko routować komunikaty, nie powinna natomiast zawierać logiki biznesowej. Pozdrawiam.

Sławek Sobótka pisze...

@Jarek: z tego co piszesz, to te "usługowe" UC wydają się być zorientowane okół jednego obiektu domenowego. Sprzedaż może pociągać za sobą generowanie Faktur, wysyłkę itd. Więc być może tutaj pojawia się dysonans a po nim dylematy.

@Paweł
No więc imho ze wszech miar nie.

1. Technicznie: skoro Saga reaguje na kilka zdarzeń, to gdzieś musi sobie choćby zapisać, które z nich już otrzymała. Po to aby otrzymując kolejne wiedziała, że ma już wszystkie.

Chyba, że masz na myśli sytuację, że saga po otrzymaniu zdarzenia, generuje sama swoje zdarzenie, w którym niesie informację, że właśnie coś w niej zaszło? Ale w sumie w jakim celu? Ktoś zainteresowany zdarzeniem sagi mógłby zamiast tego interesować się zdarzeniem oryginalnym. Chyb, że chodzi Ci o przepakowanie zdarzenia np do innego poziomu abstrakcji lub innego Bounded Context.

2. Informacyjnie: umieszczenie danych w komunikatach: to zależy. W komunikacie mam dane z momentu wystąpienia zdarzenia. Być może jest to pożądane. A być może chcę pobrać świeże wartości po ID. To zależy, ale jeżeli zdarzenie niesie komplet danych tak jak piszesz to zyskujemy decoupling.

3. Co do posiadania przez sagę logiki: w jaki sposób ma określić kiedy i gdzie routować? Chyba, że jest jedynie pośrednikiem, który nic nie wnosi, ale wówczas do czego ma służyć - liestenery mogły by sobie same poradzić. Logika może być potrzeba aby stwierdzić, czy fakt, że przyszły takie to a takie zdarzenia jest wystarczający aby "popchnąć" proces dalej.

Jeżeli sugerować się tym jak Sagi działają w Axonie, to lepiej jednak zajrzeć do posta Udiego... Axon nieco jest strywializował - chyba po to aby było wygodnie chłopakom zaimplementować automatyczne silniczki na adnotacajch, bez zwracania uwagi na funkcjonalność;)

Paweł Kaczor pisze...

Ok, przyznaję, że bezstanowość najłatwiej osiągnąć stosując komunikację w oparciu o dokumenty (document messaging). W przypadku korelacji różnych zdarzeń, czasami musimy zapisać stan procesu w Saga.
Częśćiowo sugerowałem się podejściem Rinata w toczącej się dyskusji o Sagach:
https://groups.google.com/d/topic/dddcqrs/R3HGnnuTN78/discussion

>> Logika może być potrzeba aby stwierdzić, czy fakt, że przyszły takie to a takie zdarzenia jest wystarczający aby "popchnąć" proces dalej.

Alternatywnie tę decyzję można umieścić w komponencie biznesowym, do którego prześlemy komunikat o zaistniałej sytuacji (zdarzeniu) i poczekamy na odpowiedź (zdarzenie) co zrobić dalej. Ma to sens?

>> Jeżeli sugerować się tym jak Sagi działają w Axonie, to lepiej jednak zajrzeć do posta Udiego

Tutaj mnie zgubiłeś. Mógłbyś rozwinąć myśl? Axon jak najbardziej wspiera stanowe Sagi.

Sławek Sobótka pisze...

A tutaj Udi pokazuje, że Rinat się pogubił;)
http://groups.google.com/group/dddcqrs/browse_thread/thread/4771c69e7b9337bf

Co do Axona to chodzi mi konkretnie o sposób odnajdowania konkretnego Memento. Zakłada się, że zawsze robimy po po ID pewnego agregatu, a może być w szczególności tak, że w innym module (w innym zdarzeniu) chodzi o inny obiekt. Tak jak w naszym przykładzie: raz jest to id zamówienia a innym razem wysyłki.

Paweł Kaczor pisze...

Saga w Axon może być powiązana z dowolną liczbą par (klucz, wartość). Ta sama Saga może zatem obsługiwać zdarzenie związane z wysyłką jak i zdarzenie związane z zamówieniem, w pierwszym przypadku "używając" id wysyłki, w drugim id zamówienia...

Albo to przeoczyłeś albo ja czegoś nie rozumiem.

Sławek Sobótka pisze...

Ok, wygląda na to, że przypadek, o który mi chodziło jest łatwo implementowalny przez atrybut adnotacji:
http://www.axonframework.org/docs/1.2/sagas.html

Chyba zbyt dawno nie zaglądałem do Axona:P

iirekm pisze...

Ja bym tu dodał dwie rzeczy:

- "persystentny multi-listener" mocno myli (a przynajmniej mnie zmylił) - to nie listener a eventy są tu persystowane; miałem kiedyś takie przypadki że listenera (czyli tutaj: całą sagę) trzeba było zaserializować.
Eventy zawierają tylko dane, a listenery zawierają logikę, ale często też i dodatkowe dane do obsługi tej logiki, i trochę trzeba się więcej napracować by zserializować listenera.

- eventy są naprawdę prostymi obiekcikami (getery, setery, i takie tam) - zawsze bez problemu to zaserializujemy; po co robić memento (klasy ...Data) do tych eventów?

Sławek Sobótka pisze...

Irek, ale chodzi właśnie o to, że nie eventy a stan Sagi (jej memento) są zapisywane w tym podejściu.

Zapis eventów to zupełnie inne podejście, służące do innych celów - np Event Sourcingu.

Poniekąd sam sobie odpowiedziałeś - chodzi o zapis stanu sagi, który może być czymś więcej niż stanem eventów.

Ale jest jeszcze jeden aspekt, którego nie wziąłeś pod uwagę: decupling od eventów - co można docenić gdy eventy się zmienią (w sensie kodu źródłowego) w czasie życia sagi:P

iirekm pisze...

Dobra, teraz już widzę: w jednym kroku zbieracie orderId, w innym shipmentId, a jeszcze w innym shipmentReceived=true - tutaj faktycznie saga trzyma jakiś ważny stan, choć na pierwszy rzut oka na to nie wygląda.

Nie podoba mi się to memento, no bo to oddzielenie danych od metod, czyli powrót do programowania strukturalnego, ale można ten problem załatwić za pomocą listenerów Hibernate'a albo @Configurable:

@Saga
@Configurable
@Entity
public class OrderShipmentStatusTrackerSaga {
@Inject
@Transient
private OrderRepository orderRepository;

@Column
private Long orderId;

@Column
private Long shipmentId;

...

Sławek Sobótka pisze...

Tak Saga i jej Memento to właściwie jedność. W naszej impl konkretna instancja Sagi ma zasięg prototype.

Ale jest ku temu inny powód.
Każda saga składa się z 3 klas:
- Saga jako taka - zawiera tylko logikę biznesową
- Memento - zawiera stan, manipulowany przez tą logikę
- Manager - zawiera kod techniczny (odnajdowanie Memento w bazie) oraz kod kojarzący parametry zdarzenia z Memento (za każdym razem może to być inne skojarzenie jak zauważyłeś).


Teraz pytanie: czy sklejać Sagę z Memento. Technicznie jest to proste (podałeś 2 sposoby, Configurable narzuca się automatycznie). W konkretnym projekcie - dlaczego by nie...

A w tym edukacyjnym projekcie przyjęliśmy nadrzędne założenie: żadnych magicznych sztuczek specyficznych dla frameworka w kodzie biznesowym - czyli w tym kodzie, który jest pisany przez developerów dodających ficzery biznesowe. Kod platformy - nie ma problemu, jest on w większości wypadków poza interfejsem i jest przeznaczony dla programistów o innym zakresie kompetencji.

Piotr Buda pisze...

Jesli chodzi o Rinata, to on chyba akurat implementuje Sagi jako Aggregate Root z Event Sourcingiem, wiec to nie jest bezstanowosc ;)

Paweł Kaczor pisze...

Chyba pomyliłeś Rinat'a z Jonathan'em Oliver'em

ps pisze...

Czy command handler też może obsługiwać wiele zdarzeń? Czy raczej jeden command = jeden command handler?

Sławek Sobótka pisze...

Generalnie Handler obsługuje jeden konkretny Commanda. Ew jeżeli mamy arch. multi-tenant to może mieć alternatywne Handlery per dzierżawca.

Natomiast Eventy są obsługiwane przez Listenery. Więc można by zapytać, czy jeden listener może obsługiwać wiele eventów? Potencjalnie tak, ale jednak dla dla jasności i "miejsca" na przyszłą rozbudowę zrobiłbym 1 event -> 1 (lub wiele) listenerów. A jeżeli kilka eventów aktualnie obsługuje się tak samo, to nic, bo i tak obsługę delegujemy z listenera do serwisu aplikacyjnego. Więc listener jest tylko "kodem klejącym".

Czy rozróżnioamy tutaj command (rozkaz) od eventu (oznajmienie faktu). Jest to oczywiście "jedynie" rozróżnienie semantyczne, bo technicznie oba mogą być komunikatami np na jms.

Anonimowy pisze...

pattern? persystować? Naprawdę nie udało się tutaj zastosować polskich słów?

Sławek Sobótka pisze...

naprawdę