Kapitola 14 · Vzory · Ságy a Process Managery

Ságy a Process Managery

Ságy a Process Managery v DDD a Symfony 8 – implementace kompenzačních transakcí, choreografie i orchestrace dlouhotrvajících procesů pomocí Symfony Messenger. Včetně timeoutů, paralelních kroků a monitorování distribuovaných procesů.

Autor M. Katuščák
Doba čtení ≈ 40 min
Náročnost náročná
Publikováno · Aktualizováno ·
Obsah kapitoly

V předchozí kapitole jsme se zabývali Event Sourcingem – vzorem persistence, který ukládá stav jako sekvenci neměnných událostí. Ságy na tento koncept přirozeně navazují. Event Sourcing řeší persistenci uvnitř jednoho agregátu; ságy koordinují procesy napříč více agregáty a Bounded Contexts, které spolu komunikují prostřednictvím doménových událostí.

14.01 Proč potřebujeme ságy?#

Jako ilustrativní příklad slouží typický e-shop: zákazník odešle objednávku a systém musí provést čtyři kroky napříč odlišnými Bounded Contexts:

  1. Ordering – vytvoření objednávky (agregát Order),
  2. Payment – stržení platby zákazníkovi (agregát Payment),
  3. Warehouse – rezervace zboží na skladě (agregát StockReservation),
  4. Shipping – vytvoření zásilky (agregát Shipment).

Každý z těchto kontextů má vlastní agregát, vlastní databázi (nebo alespoň vlastní tabulky se striktně oddělenou odpovědností) a vlastní invarianty, které musí chránit. Agregáty v různých Bounded Contexts nelze měnit v jedné databázové transakci – to by porušilo autonomii kontextů, jež je základním pilířem DDD. Jeden kontext nesmí sahat do databáze jiného kontextu; komunikace probíhá výhradně prostřednictvím zpráv (událostí a příkazů).

Proč nelze zabalit všechny čtyři kroky do jediné databázové transakce? Jednotlivé kontexty mohou běžet na různých serverech a používat různé databázové systémy (PostgreSQL pro objednávky, Redis pro skladové rezervace, externí platební bránu pro platby). Komunikují asynchronně přes frontu zpráv. Koncept atomické transakce se zde rozpadá.

Příklad selhání: systém úspěšně strhne platbu zákazníkovi (krok 2), ale při rezervaci skladu zjistí, že zboží není dostupné (krok 3 selže). Zákazník přišel o peníze, zboží nemá a systém je v nekonzistentním stavu. Bez mechanismu, který by tento stav detekoval a napravil, se situace sama nevyřeší – a to je v produkčním systému nepřijatelné.

Řešení tohoto problému navrhli již v roce 1987 Hector Garcia-Molina a Kenneth Salem v článku Sagas. Místo jedné velké distribuované transakce rozdělili proces na sérii lokálních transakcí, z nichž každá má definovanou kompenzační akci. Pokud některý krok selže, systém provede kompenzační akce pro všechny předchozí úspěšné kroky – v opačném pořadí. Tento vzor se v DDD komunitě ustálil pod názvy Saga a Process Manager.

Citace: Garcia-Molina, H. & Salem, K., Sagas, ACM SIGMOD (1987); Vernon, V., Implementing Domain-Driven Design (2013), kap. 4.

Část literatury oba pojmy rozlišuje: sága koordinuje proces čistě událostmi a kompenzacemi, process manager je stavová komponenta, která události překládá na příkazy. V praxi toto rozlišení splývá. Kniha proto používá „ságu“ jako zastřešující termín pro koordinátor dlouhotrvajícího procesu s perzistentním stavem a „Process Manager“ pro jeho orchestrovanou podobu ze sekce 5.

V následujících sekcích si ukážeme dva základní přístupy ke koordinaci ság – choreografii a orchestraci – a jejich praktickou implementaci v Symfony 8 s využitím Symfony Messenger.

14.02 Kompenzační transakce#

Kompenzační transakce je sémantické vrácení efektu předchozího kroku. Na rozdíl od technického rollbacku databázové transakce (který „vymaže“ změny, jako by se nikdy nestaly) je kompenzace plnohodnotná doménová operace. Má vlastní vedlejší efekty – notifikace, auditní záznamy, aktualizace stavů. Systém se nevrací do původního stavu bit po bitu, ale do takového, který je z doménového pohledu ekvivalentní situaci před provedením kompenzovaného kroku.

Pro náš e-shop scénář vypadá mapa akcí a jejich kompenzací následovně:

Akce Kompenzace Poznámka
ChargeCustomer RefundCustomer Zahrnuje notifikaci zákazníka
ReserveStock ReleaseStock Uvolnění rezervace, nikoliv smazání
CreateShipment CancelShipment Pouze do okamžiku odeslání

Kompenzace není přesný inverzní příkaz. Zatímco ChargeCustomer strhne peníze, kompenzační RefundCustomer peníze vrátí a k tomu odešle zákazníkovi notifikaci, zapíše záznam do auditního logu a může aktualizovat interní metriky. Každá kompenzace je samostatný příkaz s vlastní logikou, validací a vedlejšími efekty.

14.03 Choreografie#

Při choreografii neexistuje centrální koordinátor. Každý Bounded Context reaguje na události publikované jinými kontexty a na jejich základě provádí svůj krok procesu. Žádná služba neví o celém toku – každá zná pouze svou část a ví, na které události má reagovat.

FIG. 14.3-A Choreografie vs. orchestrace – kdo koordinuje ságu

V našem e-shop scénáři probíhá choreografická sága následovně: kontext Ordering publikuje událost OrderPlaced. Kontext Payment na ni reaguje, strhne platbu a publikuje PaymentSucceeded. Kontext Warehouse naslouchá události PaymentSucceeded, rezervuje zboží a publikuje StockReserved. Kontext Shipping reaguje na StockReserved a vytvoří zásilku, čímž publikuje ShipmentCreated. Celý tok vzniká emergentně z reakcí jednotlivých kontextů na události ostatních – bez centrálního řízení.

Hlavní výhodou choreografie je volné provázání (loose coupling) mezi kontexty. Každý reaguje pouze na události, které mu přicházejí, a o ostatních nic neví. Přidání nového kontextu (například Loyalty, který přidělí body za objednávku) nevyžaduje zásah do existujícího kódu: stačí nový handler naslouchající OrderPlaced. Procesy o dvou až třech krocích si s choreografií vystačí.

14.04 Limity choreografie#

U procesů s pěti a více kontexty nebo s podmíněným větvením narazí choreografie na čtyři problémy, které se v menším měřítku skrývají. V produkci se projeví ve chvíli, kdy do toku přibude pátý kontext nebo větvení podle stavu.

1. Neviditelný tok procesu

Při choreografii neexistuje žádné jedno místo, kde by byl celý doménový proces popsán. Tok procesu je rozdrobený do desítek handlerů v různých kontextech. Při takovém rozsahu už kompletní tok nejde vizualizovat. Nikdo nemá přehled o tom, které kroky po sobě následují, kde se proces větví a jaké jsou alternativní cesty při selhání. Vzniká fenomén, který se někdy označuje jako „distribuované špagety“ (distributed spaghetti) – analogie ke špagetovému kódu, ale rozloženému do celého systému.

2. Porušení Open-Closed Principle

Přidání nového kroku do procesu často vyžaduje úpravu existujícího kontextu. Například pokud chceme mezi platbu a sklad vložit krok „ověření proti podvodům“ (Fraud Detection), musíme změnit handler ve Warehouse. Místo události PaymentSucceeded musí naslouchat na FraudCheckPassed. Tím porušujeme Open-Closed Principle – stávající kód kontextu Warehouse je nutné upravit, aby fungoval s novým krokem. Při orchestraci by stačilo přidat krok do centrálního Process Manageru bez zásahu do existujících kontextů.

3. Obtížná diagnostika selhání

Když se proces „zasekne“, kde hledat příčinu? Každý kontext zná pouze svůj krok – neví, jaký je celkový stav procesu. Operátor musí ručně procházet logy všech kontextů, korelovat události podle orderId a rekonstruovat, kde přesně proces selhal. Neexistuje centrální dashboard, který by zobrazil: „Objednávka #42 – platba OK, sklad SELHÁNÍ, zásilka NESPUŠTĚNA.“ V produkčním prostředí s tisíci objednávkami denně je tento přístup neúnosný.

4. Chybějící timeout management

Kdo detekuje, že proces „visí“? Pokud kontext Payment strhne platbu, ale Warehouse nikdy nezareaguje (handler spadl, zpráva se ztratila ve frontě), kdo zjistí, že proces stojí? Žádný z kontextů nemá přehled o časových limitech celého procesu. V choreografii neexistuje přirozené místo pro definici globálního timeoutu – nikdo nehlídá, že celý proces od OrderPlaced po ShipmentCreated musí trvat maximálně 30 minut.

Všechny tyto problémy poukazují na jednu věc: u komplexních procesů potřebujeme centrální místo, které zná celý tok, řídí kroky, detekuje selhání a spouští kompenzace. Tuto roli plní orchestrátor – Process Manager.

14.05 Orchestrace – Process Manager#

V orchestraci celý doménový proces řídí jediná třída – tzv. Process Manager. Funguje jako stavový automat s definovanými stavy a přechody. V našem e-shop scénáři tuto roli plní OrderProcessManager. Přijímá události ze všech kontextů (Payment, Warehouse, Shipping) a na jejich základě rozhoduje, jaký příkaz vydat jako další krok. Tok není rozdrobený do desítek handlerů – celá logika procesu se soustředí do jedné třídy. Na jednom místě je viditelný kompletní tok od OrderPlaced po ConfirmOrder.

Následující diagram zobrazuje stavový automat procesu objednávky. Zelené šipky značí úspěšné přechody, červené selhání a oranžová cesta vede přes kompenzaci:

FIG. 14.5-A Stavový automat OrderProcessManager

Orchestrace přináší oproti choreografii několik výhod: celý doménový proces je popsán na jediném místě, takže vývojář okamžitě vidí kompletní tok od objednávky po potvrzení. Při debugování stačí zkontrolovat stav ságy v databázi a hned je jasné, ve kterém kroku proces stojí. Rozšíření o nový krok (například Fraud Detection mezi platbu a sklad) znamená nový stav v enumu, novou metodu pro FraudCheckPassed a úpravu onPaymentSucceeded, která místo rezervace skladu nově vydá příkaz pro kontrolu podvodů. Celá změna zůstává v jediné třídě – kontexty Warehouse ani Payment se neupravují.

14.06 Perzistence stavu ságy#

Process Manager potřebuje perzistentní úložiště stavu, aby přežil restart workeru, nové nasazení aplikace i horizontální škálování na více instancí. Bez perzistence by pád workeru mezi kroky OrderPlaced a PaymentSucceeded znamenal ztrátu informace o tom, kde se proces nachází. Sága by zůstala navždy „viset“ bez možnosti dokončení nebo kompenzace. Stav ságy proto ukládáme do databáze jako Doctrine entitu.

Entita leží v Application vrstvě, přestože Doctrine mapování jinak patří do Infrastructure (Hexagonal Architecture). Stav procesu je aplikační starost a mapování přímo na entitu šetří jednu vrstvu; kdo trvá na přísném vrstvení, přesune Doctrine část do Infrastructure.

Díky perzistenci stavu je obnova po selhání přímočará. Worker spadne uprostřed zpracování zprávy PaymentSucceeded – dřív, než ji stihne potvrdit. Po restartu Messenger tutéž zprávu doručí znovu a Process Manager načte stav ságy z databáze. Okamžitě ví, že sága čeká na platbu (AwaitingPayment), a pokračuje od správného kroku. Metoda findStale() v repository navíc umožňuje periodicky detekovat zaseklé ságy, které se déle než stanovený práh neposunuly kupředu, a spustit pro ně kompenzaci nebo eskalaci.

Multi-worker Process Manager – co se rozpadne

Optimistic lock řeší konflikt na jedné instanci ságy. V produkci se stane něco složitějšího: stejná zpráva (např. PaymentSucceeded z téže objednávky) dorazí do více worker instancí současně (Messenger numprocs > 1), nebo různé eventy z téže ságy dorazí ve špatném pořadí (Kafka partition balancing, RabbitMQ multiple consumers). Důsledky:

  • Race na vznik ságy. První OrderPlaced pro stejné orderId dorazí do dvou workerů současně. Oba vidí, že sága ještě neexistuje, a oba zavolají OrderSaga::start. UNIQUE constraint na (saga_type, correlation_id) jednoho z nich zabije, druhý zůstane. Bez constraintu vzniknou dvě paralelní ságy téže objednávky a soupeří o stav.
  • Out-of-order events. PaymentSucceeded dorazí dřív než OrderPlaced, sága ještě není ve stavu AwaitingPayment. Process Manager netuší, co s ní – buď event zahodí (bug v doméně), nebo ho zařadí do pending fronty pro pozdější zpracování (komplexní stavový automat).
  • Kompenzační závody. Sága rozhodne Compensate, vyšle RefundCustomer, a zároveň dorazí pomalá PaymentSucceeded z jiného workeru. Druhá zpráva může resetovat stav ságy z Compensating zpět na AwaitingShipment, ale RefundCustomer už běží – zákazník dostane refund, a přesto proces pokračuje k expedici.

Standardní obrana proti všem třem:

Tři stavební prvky, které zde fungují společně:

  • UNIQUE constraint na (saga_type, correlation_id) zabrání duplicitnímu vzniku ságy. Druhý INSERT vyhodí UniqueConstraintViolationException, handler ji zachytí a načte existující ságu místo vytvoření nové.
  • processedEventIds v entitě drží seznam již zpracovaných event ID. Stejný event přijde dvakrát → druhé volání skončí na guardu. To je „inbox per saga“ – paralela Idempotent Inbox z Outbox kapitoly.
  • State machine guard odmítne out-of-order event. Buď ho zahodí (idempotentně), nebo ho zařadí do pending events sloupce pro pozdější aplikaci.

Distributed deadlock mezi ságami

Klasický dvouagregátový deadlock přes Doctrine pessimistic lock: sága A drží lock na Order#1 a žádá o Inventory#42; sága B drží lock na Inventory#42 a žádá o Order#1. Postgres deadlock detector po cca 1 s jednu z transakcí zabije, ale do té doby čeká celý connection pool a workery stojí.

S eventual consistency (Vernonovo „eventual consistency mimo hranici agregátu“, viz Návrh agregátu) deadlock nemůže nastat na úrovni databáze – každý krok ságy je samostatná transakce na jeden agregát. Jiný typ deadlocku ale možný je: logický cycle deadlock v sáze samotné.

Příklad: sága OrderProcess čeká na PaymentSettled. Sága RefundProcess (pro storno) čeká na OrderCancelled. Pokud kompenzace způsobí storno objednávky a zároveň zrušení refundu, obě ságy čekají na sebe a žádná nedokončí.

Recovery z nekonzistentního stavu ságy

Sága může skončit v nekonzistentním stavu z legitimních příčin: deployment během transakce, OOM kill v polovině compensation kroku, schema migration změnila tvar state JSONu. Operátor potřebuje tři nástroje. Prvním je read-only inspekce: CLI command app:saga:show <id> vypíše aktuální stav, čekající události, zpracovaná event ID a počet pokusů, plus odkaz na ságu v Grafaně. Druhým je manuální přechod: app:saga:force-transition <id> <to> s povinným --reason="..." aktualizuje status, zapíše audit log a invaliduje čekající události; je určen výhradně operátorům – jeho použití signalizuje bug v sáze nebo neošetřený doménový scénář. Třetím je replay od checkpointu: u idempotentní ságy (a taková by měla být – viz výše) smazání stavu a přehrání všech jejích eventů z outbox/event store obnoví správný stav. Vyžaduje sledování správného počátečního eventu (typicky OrderPlaced event ID).

Izolace ság: ACD bez I

Databázová transakce dává ACID. Sága jen ACD – atomicitu přes kompenzace, konzistenci a trvanlivost. Izolace chybí: každý commitnutý krok je okamžitě viditelný všem souběžným procesům, dlouho předtím, než celá sága skončí. Jiný proces nad stejnou objednávkou nebo skladem může rozpracovaný stav přečíst i přepsat. Vznikají anomálie známé z databází – lost update (storno ságy přepíše změnu, kterou objednávková sága právě provádí) a dirty read (proces si přečte platbu, kterou kompenzace vzápětí vrátí).

Richardson pro tyto anomálie popisuje sadu protiopatření (countermeasures). První dvě pracují s daty. Semantic lock je aplikační zámek: záznam, na kterém sága pracuje, nese stav s příznakem *_PENDING a ostatní procesy ho musí respektovat. Commutative updates jsou operace navržené tak, aby na pořadí nezáleželo – připsání a odepsání částky komutuje, nastavení absolutního zůstatku ne.

Zbylá dvě pracují s průběhem ságy. Pessimistic view přeuspořádává kroky: změna, jejíž dirty read napáchá největší škodu (třeba připsání kreditu), se přesune za pivot transakci (viz Když selže kompenzace). Při reread value si krok před zápisem hodnotu znovu načte a ověří, že se od prvního čtení nezměnila; jinak ságu zastaví nebo opakuje.

Semantic lock je z nich nejčastější:

Kolizní požadavky lze místo výjimky také frontovat a provést po uvolnění zámku; pro většinu domén ale stačí odmítnutí výjimkou a opakování na straně klienta.

Citace: Richardson, C., Microservices Patterns (2018), kap. 4.

14.07 Implementace v Symfony Messenger#

Předchozí sekce ukázaly Process Manager (orchestrátor) a perzistenci stavu ságy. Nyní propojíme obě části s Symfony Messenger – asynchronním message busem, který zajistí spolehlivé doručování událostí a příkazů mezi kontexty. Základní konfigurace Messenger busů je popsána v kapitole CQRS – Symfony Messenger. Zde se zaměříme na specifika pro ságy: oddělené transporty pro události a příkazy a retry strategie, bez kterých dlouhotrvající procesy ztrácejí zprávy při běžných výpadcích.

Celý tok funguje následovně: agregát Order v kontextu Ordering publikuje událost OrderPlaced na event bus. Messenger ji podle konfigurace routingu odešle do transportu async_events. Worker naslouchající na tomto transportu zprávu vyzvedne a předá ji OrderProcessManager, který ji zpracuje metodou onOrderPlaced(). Ta uloží stav ságy a dispatchne příkaz ChargeCustomer na command bus. Messenger tento příkaz routuje do transportu async_commands, kde ho vyzvedne handler v kontextu Payment. Po úspěšném zpracování Payment publikuje PaymentSucceeded – a cyklus se opakuje pro další krok procesu.

Podrobnější informace o asynchronním zpracování zpráv, konfiguraci transportů a retry strategiích najdete v kapitole CQRS – asynchronní zpracování.

14.08 Timeouty a deadliny#

Co se stane, když událost PaymentSucceeded nikdy nedorazí? Síťový výpadek, nedostupnost platební brány, ztráta zprávy ve frontě – v distribuovaném systému musíte vždy počítat s tím, že odpověď nepřijde. Bez explicitního timeout mechanismu sága zůstane navždy ve stavu AwaitingPayment a objednávka se nikdy nedokončí ani nezruší. Proto potřebujeme timeout check – odložený příkaz, který po uplynutí stanovené doby zkontroluje, zda se sága posunula dál. Pokud ne, ságu ukončí, nebo spustí kompenzaci – podle toho, zda už proběhl krok, který je co vracet.

Timeout check naplánujeme přímo v Process Manageru při zpracování události OrderPlaced. Messenger nabízí DelayStamp, který zprávu podrží v transportu po zadanou dobu a teprve poté ji doručí workeru:

14.09 Kompenzační strategie v praxi#

Když krok ságy selže, máme dvě základní strategie, jak situaci řešit. Volba závisí na povaze chyby – je přechodná (síťový výpadek, dočasná nedostupnost služby), nebo trvalá (nedostatek prostředků na účtu, zboží vyprodáno)?

Forward recovery (retry)

Při přechodných chybách je nejjednodušší strategií opakování – pokus o provedení stejného kroku znovu. Symfony Messenger nabízí vestavěnou retry strategii s exponenciálním backoffem, kterou jsme konfigurovali v sekci 7. Worker automaticky opakuje selhané zprávy podle nastavení max_retries, delay a multiplier. Tento přístup je vhodný, když věříme, že problém je dočasný a opakování může uspět.

Backward recovery (kompenzace)

Při trvalých chybách (selhání s doménovou příčinou) musíme spustit kompenzaci – vrátit systém do konzistentního stavu provedením kompenzačních akcí v opačném pořadí dokončených kroků. Kompenzace je sémantická, nikoli technická. Neděláme DELETE FROM payments – místo toho dispatchujeme nový doménový příkaz RefundCustomer, který vytvoří novou transakci (refund). Každá kompenzační akce je plnohodnotná doménová operace s vlastními pravidly a událostmi.

FIG. 14.9-A Kompenzační flow – rollback ságy v opačném pořadí

Když selže kompenzace

Metoda onStockReservationFailed ze sekce 5 dispatchuje RefundCustomer a převede ságu do stavu Compensating. Tam sága zůstává. Refund je asynchronní příkaz – do terminálního Failed smí přejít až poté, co dorazí potvrzení RefundSucceeded. Přechod do Failed hned po dispatchi by ságu uzavřel dřív, než refund proběhl; při jeho selhání by se po penězích zákazníka nikdo nesháněl. Stav „kompenzace odeslána, čeká se na potvrzení“ se označuje jako compensation pending.

Richardson (Microservices Patterns, 2018, kap. 4) dělí kroky ságy do tří skupin:

  • Compensatable transactions – kroky před pivotem; každý má definovanou kompenzaci (ChargeCustomerRefundCustomer).
  • Pivot transaction – bod rozhodnutí. Jakmile commitne, sága už necouvá a poběží dopředu až do konce. V našem procesu je pivotem rezervace skladu.
  • Retryable transactions – kroky po pivotu (CreateShipment, ConfirmOrder). Kompenzaci nemají, nesmí selhat z doménových důvodů a opakují se až do úspěchu.

Kompenzace samy patří do třetí kategorie. RefundCustomer nemá legitimní doménový důvod selhat – peníze, které systém strhl, musí umět vrátit. Selhání je vždy technické: nedostupná platební brána, timeout, chyba sítě. Odpovídá tomu strategie: retry s exponenciálním backoffem (retry_strategy ze sekce 7), po vyčerpání pokusů zpráva končí ve failure transportu, systém odešle alert a objednávka putuje do fronty ručních zásahů. Ságu visící v Compensating zachytí i detekce zaseklých ság ze sekce 11.

Podrobnější informace o Dead Letter Queue, retry strategiích a zpracování chyb v Messenger najdete v kapitole CQRS – zpracování chyb.

14.10 Paralelní kroky#

Dosud jsme uvažovali sériové provádění kroků – jeden po druhém. V praxi však některé kroky na sobě nezávisí a mohou běžet současně. Například po úspěšné platbě chceme zároveň rezervovat zboží na skladě a vygenerovat fakturu. Obě operace jsou nezávislé – výsledek jedné neovlivňuje druhou. Paralelním zpracováním zkrátíme celkovou dobu trvání ságy.

Princip: sága dispatchuje oba příkazy současně a přejde do stavu AwaitingStockAndInvoice. V kontextu si uchovává dva příznaky (stockReserved a invoiceCreated). Teprve když oba dorazí jako splněné, sága pokračuje dalším krokem – vytvořením zásilky. Tomuto vzoru se říká synchronizační bariéra (synchronization barrier). Stav AwaitingStockAndInvoice v enumu OrderSagaStatus ze sekce 5 zatím chybí – paralelní varianta vyžaduje doplnit nový case.

14.11 Monitoring a observabilita#

Bez monitoringu se zpráva ztratí ve frontě, stav ságy zamrzne a nikdo si ničeho nevšimne, dokud si zákazník nestěžuje. Produkční sága proto potřebuje vědět, které instance právě běží, které se zasekly a které selhaly. Dva nástroje, které tuto viditelnost zajišťují: korelační ID pro trasování a detekce zaseklých ság.

Korelační ID

Každá zpráva v jedné sáze nese stejné korelační ID – typicky orderId. Díky němu můžete v logu vyfiltrovat všechny zprávy patřící ke konkrétní objednávce a sledovat celý průběh procesu od začátku do konce. Více o korelačních identifikátorech najdete v glosáři.

Technicky se korelace řeší vlastním stampem (např. CorrelationIdStamp), který sága připojí na envelope při dispatchi a logovací middleware ho čte ze stampu místo spoléhání na konkrétní pole zprávy. Envelope tak nese korelaci i pro zprávy, které žádné orderId nemají.

Detekce zaseklých ság

I při nejlepším návrhu se stane, že zpráva se ztratí, worker spadne nebo externí služba přestane odpovídat. Proto potřebujete cron/scheduled command, který pravidelně kontroluje, zda některá sága nezůstala příliš dlouho v mezistavech:

Podrobnosti o implementaci middleware v Symfony Messenger najdete v kapitole CQRS – sekce middleware.

14.12 Testování ság#

Chyba v přechodové logice nebo v kompenzacích se projeví až v produkci – stržená platba bez doručeného zboží, duplikované zásilky a podobně. Testujeme na třech úrovních.

Unit testy stavového automatu

Nejdůležitější úroveň: testujeme samotný Process Manager izolovaně od infrastruktury. Místo skutečného message busu použijeme spy implementaci, která zaznamenává dispatchované příkazy, a místo databáze in-memory repozitář:

Další vzory pro testování doménové logiky, agregátů a event handlerů najdete v kapitole Testování DDD aplikací.

Časté otázky

Jaký je rozdíl mezi Ságou a Process Managerem?

Sága je obecný pojem pro dlouhotrvající transakci napříč více službami, rozdělenou na sérii lokálních transakcí propojených kompenzacemi. Process Manager je konkrétní implementační styl ságy – centralizovaná komponenta s vlastním stavem, která orchestruje kroky zasíláním příkazů a reaguje na přicházející události. Tyto dvě osy (Sága/Process Manager vs. choreografie/orchestrace) jsou ortogonální: sága může být choreografická i orchestrovaná, Process Manager je vždy orchestrátor. V některé literatuře se ovšem pojmem sága myslí spíš choreografická varianta a orchestrátor se označuje jako Process Manager (vzor z Enterprise Integration Patterns Hohpeho & Woolfa); Richardson naopak používá ságu pro choreografickou i orchestrovanou podobu – terminologii je proto vhodné v každém zdroji ověřit. Rozbor rozdílů v sekci Orchestrace – Process Manager.

Choreografie, nebo orchestrace – kdy zvolit co?

Choreografie, kde služby reagují na události publikované ostatními, se hodí pro krátké procesy o dvou až třech krocích, kde je spojení mezi službami volné a globální stav není kritický. Orchestrace přes Process Manager je vhodnější pro složitější procesy s rozhodovací logikou, časovými limity nebo nutností centralizovaně znát stav běhu. Pro procesy přes více než tři kroky nebo s podmínkami je orchestrace obvykle udržovatelnější. Rozhodovací kritéria v sekci Limity choreografie.

Jak implementovat kompenzační transakce v Symfony?

Kompenzace je samostatná operace nebo command handler, který vrací systém do stavu před selhaným krokem – například CancelPayment jako protějšek AuthorizePayment. V Messenger sáze se kompenzace spouští, když příchozí událost signalizuje selhání některého z pozdějších kroků. Kompenzační příkazy musí být idempotentní a tolerantní k situaci, že kompenzovaný krok nikdy neproběhl. Ne každou operaci lze technicky vrátit, proto se někdy kompenzuje jiným způsobem. Praktický příklad v sekci Kompenzační strategie v praxi.

Jak zajistit idempotenci ságy při opakovaném doručení událostí?

Messenger může stejnou zprávu doručit vícekrát – při selhání workera nebo přebalení na retry queue – takže handler musí opakované zpracování bezpečně ignorovat. Standardní řešení jsou dvě: jedinečný identifikátor zprávy uložený do tabulky zpracovaných ID, nebo stavový automat ságy, který u každého kroku kontroluje, zda už není ve stavu „dokončeno“. Obě techniky brání duplicitnímu publikování příkazů i duplicitním kompenzacím. Podrobný rozbor v sekci Implementace v Symfony Messenger.

Má se sága obsluhovat přes Command Bus, nebo Event Bus?

Obojí, s jasně rozdělenou rolí. Události na Event Busu spouštějí reakce ságy – informují, že se něco stalo, a sága na ně navazuje. Příkazy na Command Busu sága sama vydává, aby řídila další kroky. Typická smyčka má tvar: příchozí event → Process Manager → odchozí command → handler → nový event. Nikdy se nezaměňuje: event nic nepřikazuje, command nic neoznamuje. Viz sekci Implementace v Symfony Messenger.