Fixy na produkcji i jak (nie) wersjonować zdarzenia
Cześć!
Szczęśliwi, którzy nie byli na produkcji. Mogą sobie tacy żyć w swoim własnym wyimaginowanym świecie. Jest to taki świat, w którym nie ma problemów. Wszystko jest czarno-białe. Jest to świat gdzie można toczyć dysputy czy lepiej użyć takiego wzorca czy innego. Czy takie podejście jest lepsze czy inne. Produkcja zmienia wszystko. Dopóki człowiek nie znajdzie się w takiej sytuacji, to ciężko mu jest sobie to wyobrazić. Gdy już to nastąpi i dostajemy informację, że “Houston mamy problem” to nasze wszystkie akademickie dyskusje i wyższe ideały okazują się mniej ważne. Trzeba ratować to co się ma i podejmować zgniłe kompromisy, których się potem często wstydzimy. Oczywiście są to historie, które opowiemy znajomych, ale już na rozmowie kwalifikacyjnej niekoniecznie. Hot fix na produkcji. W piątek wieczorem.
Nie każdemu się takie hotfixy udają, często jest tak, że fix, który miał problem załatać jeszcze bardziej ten problem eskalują. Piątkowe hotfixy się praktycznie nigdy nie udają. A te w czwarkowe popołudnie? Jeszcze rzadziej.
Jednym z najczęstszych pytań, które dostaję gdy opowiadam ludziom o Event Sourcing jest:
“Jak wesjonować zdarzenia?”
Jest to trudne pytanie, Greg Young napisał o tym książkę (którą każdemu polecam https://leanpub.com/esversioning/read).
Praktyk jest wiele:
- migracja danych,
- rozszerzenie aktualnego schematu w sposób nie robiący “breaking change” (np. dodanie nowego pola),
- dodanie nowego schematu ze zmienioną nazwą lub namespace,
- upcasting - gdzie przy odczycie starego zdarzenia podpinamy mapper pomiędzy serializacją, a samym jego zaaplikowaniem i transformujemy stare w nowe,
- zapis/publikowanie zarówno starego jak i nowego zdarzenia, dopóki są jeszcze “nasłuchujący”, którzy stary schemat obsługują.
- i wiele innych.
Każda z tych praktyk ma wady i zalety, w jednych sytuacjach sprawdzi się lepiej, w innych gorzej.
Ja jednak z każdym miesiącem dochodzę do wniosku, że najlepszą opcją na wersjonowanie schematu zdarzeń jest niedopuszczanie do sytuacji gdy takowe wersjonowanie zdarzeń będzie potrzebne.
Kolejny suchar z których jestem znany? Nie tym razem.
Zastanówmy się kiedy takie wersjonowanie jest nam potrzebne. Potrzebne jest nam ono gdy wdrażamy nową wersję sytemu z nową wersją schematu zdarzeń. Jeśli mamy w event store (bazie danych) zdarzenia w starym schemacie to wypadałoby być w stanie je obsłużyć.
Takie obsłużenie wiąże się zwykle z koniecznością dodania dodatkowego kodu - takiego, który obsłuży nam nowy schema zdarzenia i pozostawieniu starego kodu, który obsłuży stare. Migracja zdarzeń jest technicznie możliwa, ale wiadomo - z założenia niezalecana, bo nie powinniśmy tych zdarzeń zmieniać.
Problem dotyczy oczywiście starych zdarzeń, bo te nowe już będziemy publikować w nowym schemacie i dla nowych strumieni (obiektów) wszystko już będzie cacy. Także wszystko by było elegancko, gdyby nie te stare zdarzenia - może by się dało ich jakoś pozbyć? Tylko jak to zrobić?
Sposobów jest kilka:
- Summary Event - jest to takie zdarzenie typu “snapshot”. Posiada ono aktualny stan agregatu. Możemy takie zdarzenie utworzyć i zapisać np. w trakcie wdrożenia już na podstawie nowej wersji kodu. Wtedy możemy traktować takie zdarzenie jako punkt początkowy naszego strumienia. Dzięki temu, możemy w zasadzie zignorować zdarzenia, które wydarzyły się przed nim. Publikujemy nowy snapshot, od tego momentu wszystkie zdarzenia lecą już po nowemu, stare moglibyśmy nawet zarchiwizować. Jakie są wady tego podejścia? Takie zdarzenie jeśli będzie zwykłym zdarzeniem podsumowującym. Nie niesie żadnej informacji biznesowej. Jeślibyśmy zarchiwizowali pozostałe zdarzenia to moglibyśmy mieć problemy przy odbudowie projekcji. Oczywiście można mieć kilka tego typu zdarzeń na różne okazje, ale jednak jest to pewnego rodzaju zaciemnienie sytuacji.
- Transformacja strumienia zdarzeń - jest to scenariusz opisany w naszej martenowej dokumentacji: https://martendb.io/documentation/scenarios/copyandtransformstream/. Polega to na tym, że w trakcie wdrożenia bierzemy strumienie, które mają starą wersję schematu zdarzeń. Kopiujemy je, transformujemy i zapisujemy jako nowy strumień. Do starego dodajemy zdarzenie “nagrobkowe” (tombstone event), które niesie informację jaki jest nowy identyfikator strumienia. Coś jak Redirect w HTTP. Dzięki temu dostajemy stare zdarzenia przemigrowane do nowego schematu. Stare możemy sobie nawet zarchiwizować i nie musimy utrzymywać dwóch implementacji. Minusem jest oczywiście to, że chronologia zdarzeń jest nieco zaburzona. Stare zdarzenia pojawiają się bowiem w nowym miejscu. Tam gdzie globalna kolejność zdarzeń jest kluczowa może to być problemem. Nadmienię tutaj, że dlatego warto wszystkie pola biznesowe, nawet jeśli można je wyczytać z metadanych zdarzenia trzymać też jako dane zdarzenia. Wtedy np. data potwierdzenia rezerwacji będzie stała, nawet jak w metadanych data utworzenia zdarzenia będzie inna.
- Tworzenie małych agregatów - nie mówię tutaj o samym kodzie. To każdy wie, że mniej kodu jest lepsze niż większe. To o czym mówię tutaj to to, żeby tworzyć małe agregaty, które żyją krótko. Weźmy np. proces zamówienia w sklepie internetowym. Moglibyśmy zamodelować jeden wielki agregat typu zamówienie, gdzie po kolei robilibyśmy obsługę dodawania do koszyka, potwierdzania go, potem opłacenia, wysłania produktów, na końcu zamykania i uzyskania potwierdzenia. Moglibyśmy również zamiast jednego agregatu zrobić kilka np. koszyk zamówień, potwierdzone zamówienie, przesyłka, opłata, zakończone zamówienie. To samo np. w rezerwacji hotelowej. Zamiast wspierać cały proces rezerwaji, pobytu gościa i rozliczenia go jako jeden byt, moglibyśmy podzielić na małe agregaty typu. Tymczasowa rezerwacja, potwierdzona rezerwacja, pobyt gościa, opłacona rezerwacja itd. Co dzięki temu zyskujemy? Jeżeli nasz agregat będzie żył krótko - załóżmy dzień/dwa/tydzień to gdy mamy wdrożenie i mamy jakieś otwarte byty ze starym schematem zdarzeń to najprawdopodobniej będziemy musieli je wspierać co najwyżej te kilka dni. Dzięki temu możemy rozbić nasze wdrożenie na “dwa takty”. Najpierw wdrażamy wersję, która wspiera obydwa schematy i oznaczamy stary jako “obsolete”. Następnie przy kolejnym wdrożeniu pozbywamy się kodu odpowiadającego za stare zdarzenia, bo nie ma już żadnych aktywnych instancji, które go mają. Oczywiście nie zawsze się tak da, niektóre byty żyją dłużej, ale wtedy możemy zastosować jedną z technik opisaną wcześniej.
Co o tym sądzisz? Masz jakieś pytania?
Na koniec trzy newsy z mojego poletka:
- moje repozytorium z przykładami do Event Sourcing (https://github.com/oskardudycz/EventSourcing.NetCore) ma już 600 gwiazdek! Cieszę się bardzo, bo jest to jakiś mały powód do radości w tych niezbyt wesołych czasach. Pamiętam jak jeszcze niedawno chwaliłem się żonie, że mam 10 gwiazdek. Super!
- w zeszłym tygodniu dostałem przelew 1324 zł za swoją działalność społecznościową w ramach Github Sponsors. Jest to pierwsza wypłata od kiedy dołączyłem tam 6 lipca tego roku. Przez ten czas 5 osób na tyle doceniło moją pracę, że postanowiło mnie wspierać mnie i moją działalność. Jest to dla mnie wielka radość i też docenienie! Dodatkowo Github wyrównuje wpłaty wspierających, więc dzięki temu dostałem drugie tyle co dostałem od moich Sponsorów. Github będzie to robił dopóki suma wpłat nie przekroczy 5000$ - więc jeszcze trochę powyrównuje. Póki co nie są to oczywiście zawrotne kwoty, nie są w stanie zapewnić mi utrzymania, ale mieć a nie mieć? Wiadomo. Plus dodatkowo jest to świetna motywacja dla mnie, że warto bo momenty zwątpienia przychodzą często. Jak człowiek widzi, że go ktoś docenia to tym bardziej. **Jeśli chcesz mnie wesprzeć to zapraszam na: **https://github.com/sponsors/oskardudycz.
- jest dostępne nagranie z mojego wystąpienia na Bydgoskiej Grupie .NET - zaspoileruję, ale udało mi się zrobić po raz drugi Event Store w godzinę. Jeśli chcesz zobaczyć jak to zrobiłem - oto i ono: https://www.facebook.com/watch/live/?v=265385864813980. Polecam obejrzeć też prezentację Marcina Suleckiego, bardzo ciekawie pokazał Redis z innej strony.
Pozdrawiam serdecznie!