Programio

Antipatterny Event sourcingu

V minulém článku jsem představil event sourcing a popsal, jak ho u nás v práci používáme. Dnes se podíváme na to, jaké problémy nám přechod na event sourcing přinesl a jak jsme je vyřešili a co jsme si tak nějak označili jako anti-patterny, kterým je lepší se vyhnout.

Neidempotence

Od Událostí očekáváme, že jejich aplikace bude idempotentní. Idempotence znamená, že

1
∀x∈D(f): f(x)=f(f(x))

Méně srozumitelně: pokud stejnou Událost aplikujeme na entitu vícekrát, nesmí to změnit stav entity. Nejlépe se to vysvětluje na příkladu: “zvyš mzdu programátora o deset tisíc” není idempotentí Událost, “nastav plat programátora na sto tisíc” je idempotentí Událost. Pokud první Událost aplikujeme pětkrát, zvýšili jsme mzdu programátora o padesát tisíc. U druhého typu Událostí to nehrozí.

Smyslem tohoto pravidla je umožnit zpracovat například posledních sto Událostí znova bez toho, aniž by to rozbilo celý systém a také se to lépe debuguje – zkrátka kouknete na Událost a hned víte, co uživatel nastavil a jaký je aktuální stav systému.

Stejně tak bychom nikdy neměli nic vyvozovat z počtu daných Událostí. Pokud bychom například chtěli uživateli zobrazovat, kolikrát se už pokusil změnit název Kampaně, nemůžeme to udělat tak, že po každé, když se zavolá metoda applyCampaignNameSet inkrementujeme proměnnou, protože se teoreticky mohlo stát, že se metoda applyCampaignNameSet zavolala dvakrát se stejnou Událostí a už bychom to zobrazovali špatně.

Místo toho tak musíme v takovém případě mít informaci o počtu Událostí přímo v datech dané Události. Vyprodukujeme-li třetí Událost CampaignNameSet pro danou Kampaň, uložíme přímo do Události, že je třetí v pořadí. Všechny View Buildery si pak mohou informaci o počtu Událostí daného typu přečíst přímo z Události.

Neznámé Události

Občas se nám stalo, že jsme releasli novou verzi aplikace, která vyprodukovala Událost, kterou ostatní aplikace neznaly. Třeba jsme přidali kód, který automaticky hlídal konce kampaně a když tento konec nastal, vypálila se Událost CampaignStateChanged. Kvůli nějaké chybě se stalo, že tenhle kód šel do světa dříve než aktualizace View Builderů, takže když se potom o půlnoci vyprodukovaly první Události CampaignStateChanged, které některým Kampaním nastavily stav na “Finished”, celý zbytek systému byl těmito Událostmi zmaten, protože tuto Události neznal. Nabízí se v zásadě dvě řešení:

  • Neznámé Události přeskočit. S tím je ten trabl, že ty překočené Události už se nikdy znova nezpracují. Jako by neexistovaly – pokud ručně neresetujeme View Builder, aby zpracoval všechny Události znova. Do té doby, než bychom resetovaly View Builder, by se tak daná Kampaň jevila jako neukončená celému systému, což jistě není správně.

  • Druhou možností je, že když aplikace narazí na Událost, kterou nezná, tak spadne nebo přinejmenším přestane číst další Události. To taky není úplně šťastné řešení. Představme si, že ta nová neznámá Událost je třeba CampaignNameSet. Takovému Jádru, které vybírá, jakou reklamu uživateli zobrazit, je název Kampaně úplně šumák, takže by bylo smutné, kdybychom kvůli této neznámé Události přestali číst další Události. Třeba hned v další Události někdo chtěl snížit cenu za proklik – a my bychom tuto změnu neaplikovali jenom proto, že se uživatel snažil přejmenovat Kampaň a Jádro nevědělo co s tím.

Celá věc se komplikuje tím, že chceme být obecně schopni releasovat kdykoliv jakoukoliv část systému bez toho, aniž by to ohrozilo ostatní. Zatím je nicméně naše řešení takové, že aplikace spadne nebo se zastaví čtení dalších Událostí, pokud natrefí na Událost, kterou nezná a programátoři musí daný problém rychle vyřešit.

Propagace Událostí

Mějme Stránku, která má pět Sekcí, které zase mají ještě pět Sekcí. To je celkem třicet Sekcí pod jednou hlavní Stránkou. A teď co se má stát, když uživatel nastaví Stránce, že se tam nesmí zobrazovat pornografický obsah? Toto nastavení se typicky dědí i do sekcí. My samozřejmě vypálíme Událost SiteRestrictionSet, otázkou ale je, jestli máme také vypálit třicet Událostí SectionRestrictionSet pro každou Sekci zvlášť.

  • Když to neuděláme a necháme jen tu jednu Událost SiteRestrictionSet, tak všechny aplikace, které reagují na tuto Událost, budou muset po svém rešit logiku dědění, tj. budou muset řešit problém “Já sice nemám nastaveou žádnou restrikci, ale můj prapředek ano”. Tuto logiku bychom museli více méně rozkopírovat všude, kde se vytváří nějaký model se Stránkami.

  • Když to uděláme a všem Sekcím řekneme pomocí Událost SectionRestrictionSet, že se jim změnilo nastavení, nemusíme už nikde nic zvláštního doprogramovávat, protože příslušné applySectionRestrictionSet metody už všude máme. Na druhou stranu to znamená, že místo jedné Události jich budeme mít třicet. Přitom existují vlastnosti, které se budou měnit daleko častěji a dynamičtěji než zrovna nastavení restrikcí.

    • Předchozí problém s hromadou Událostí by teoreticky šel řešit tak, že dovolíme, aby jednu stejnou Událost mohlo zpracovat více entit. Zatím to máme tak, že jedna Událost patří vždy k jedné entitě. Mohli bychom upravit náš event framework tak, abychom mohli říct, že jedna Událost SectionRestrictionSet může “patřit” několika entitám zároveň.

Zatím nemáme jednotný způsob jak podobné případy řešit, někdy to vyřešíme propagací Událostí, někdy ne. Postupem času se ale asi více přikláníme k první možnosti a Události spíše propagujeme. Dá se to lépe zautomatizovat a množství Událostí nám zatím takový problém nedělá. Ale možná jednou začne.

Máte s Event sourcingem více zkušeností? Jak podobné problémy řešíte vy?