Outbox a worker
Jak uložit data a zprávu „naráz“ – zpráva leží v téže databázi a doručí ji proces na pozadí.
Na této stránce
V jedné větě: Zpráva se neposílá hned, ale uloží se do tabulky
OutboxMessagesve stejné transakci jako data. Proces na pozadí ji pak doručí – a když se to nepovede, zkusí to znovu.
Jaký problém řeší (dual write)
Uložit objednávku do databáze a poslat zprávu do RabbitMQ jsou dvě různé operace. Bez outboxu:
- uložím objednávku → aplikace spadne → zpráva nikdy neodejde (sklad o objednávce neví),
- pošlu zprávu → uložení selže → sklad rezervuje zboží pro objednávku, která neexistuje.
Distribuovaná transakce přes DB a broker v praxi nefunguje (a SQLite ani RabbitMQ ji neumí).
Přirovnání: Podací kniha na poště. Dopis nenesete rovnou adresátovi – zapíšete ho do knihy spolu s ostatními papíry v jedné složce. Pošťák chodí každé dvě sekundy, bere zápisy z knihy a odškrtává doručené.
Jak to funguje v MiniShopu
sequenceDiagram
participant S as OrderService
participant DB as ordering.db
participant W as OutboxWorker (každé 2 s)
participant P as OutboxProcessor
participant T as Cíl (handler / RabbitMQ)
S->>DB: BEGIN
S->>DB: INSERT Orders
S->>DB: INSERT OutboxMessages (OrderPlaced)
S->>DB: COMMIT – obojí, nebo nic
W->>P: ProcessPendingAsync()
P->>DB: SELECT nezpracované (max 20, méně než 5 pokusů)
loop každá zpráva ve vlastním scope
P->>T: doruč (doménová → dispatcher, integrační → bus)
alt úspěch
P->>DB: UPDATE ProcessedAtUtc = teď
else handler selhal
P->>DB: Attempts + 1, Error (zkusí se v dalším kole)
else broker nedostupný
P->>DB: jen Error – nepočítá se jako pokus
end
end
Kód: ModuleDbContext.SaveChangesAsync (uložení), OutboxWorker (časovač), OutboxProcessor (doručení), OutboxMessage (řádek tabulky).
Důležité detaily
- Každý modul má vlastní outbox ve své databázi. Jinak by zpráva nebyla v téže transakci jako data.
- Dva druhy zpráv (
Kind): doménová jde handlerům ve stejné službě, integrační ven přes broker. - Retry bez backoffu: neúspěšná zpráva se zkusí v dalším kole (za 2 s), maximálně 5×. Pak je to poison message – zůstane v tabulce s chybou a čeká na člověka.
- Výpadek brokeru se nepočítá jako pokus – chyba není ve zprávě, zprávy počkají, až se RabbitMQ vrátí.
- Doručení „alespoň jednou“ (at-least-once): když aplikace spadne po doručení, ale před označením, zpráva odejde znovu. Proto příjemce potřebuje inbox.
- Trasování: zpráva si v
TraceParentnese ID trasy, ve které vznikla – viz Observabilita. - Health check:
/healthhlásí Degraded, když zpráva visí déle než 5 minut (OutboxHealthCheck).
Známá omezení
- Worker počítá s jednou instancí služby. Při více instancích by bylo potřeba zprávy zamykat.
- Zpracované zprávy se nemažou (tabulka roste) – pro výuku to nevadí, v produkci by se čistila.
- Každý krok ságy čeká na kolo workeru, proto potvrzení objednávky trvá několik sekund.