Ne všechno musí proběhnout, dokud uživatel čeká na odpověď. Když si někdo založí účet, nemá důvod čekat, až se opravdu odešle uvítací e-mail. Když nahraje video, nemá čekat na jeho zpracování. A některé věci se mají stát samy v určitý čas (noční zálohy, měsíční faktury). Tahle kapitola je o práci, která běží na pozadí — mimo cestu requestu.
Pár pojmů na úvod
- Job (úloha) = jeden kus práce, který se má udělat na pozadí (poslat e-mail, zpracovat obrázek).
- Worker = proces, který si bere úlohy z fronty a zpracovává je.
- Scheduler = něco, co spouští úlohy v naplánovaný čas (např. každou noc ve 3:00).
- Cron = klasický nástroj na plánování opakovaných úloh podle času.
Tahle kapitola staví na Messaging (fronty) — pokud jsi ji nečetl, začni tam.
Proč nedělat všechno hned v requestu
Dlouhá nebo odložitelná práce v cestě requestu má dvě nevýhody: uživatel zbytečně čeká, a když ta práce selže, spadne mu celý request. Lepší je práci odložit na pozadí: requestu rovnou odpověz „přijato, zpracovávám" a samotnou práci nech udělat workera později.
Dva druhy úloh na pozadí:
- Spouštěné událostí (triggered) — vznikly z nějaké akce (uživatel nahrál soubor → zpracuj ho). Typicky přes frontu (úkol se vloží, worker si ho vezme).
- Naplánované (scheduled) — mají běžet v určitý čas, opakovaně (noční záloha, ranní souhrn). Typicky přes scheduler/cron.
Co musí dobrá úloha na pozadí splňovat
- Idempotence — úloha může proběhnout vícekrát (worker spadne a úkol se zopakuje, fronta doručuje at-least-once). Musí to skončit stejně, jako by proběhla jednou (viz Patterny).
- Opakování při selhání (retry) — když úloha selže (dočasný výpadek), zkus to znovu s backoffem. Po několika marných pokusech ji odlož do DLQ (Messaging).
- Viditelnost — uživatel/admin by měl vidět stav (čeká / běží / hotovo / selhalo).
- Omezená souběžnost — pusť jen tolik workerů, kolik unese cíl (Concurrency).
Plánované úlohy (scheduling) a jejich zrada
Naivně: na jeden server dáš cron, který spustí skript. Funguje, dokud máš jeden server. Problém
přijde s víc servery: pokud běží cron na všech, úloha (např. „naúčtuj měsíční platby") se spustí
vícekrát — jednou na každém serveru. Řešení:
- Leader election — jen jeden server (zvolený „vedoucí") úlohy spouští (viz Distribuované systémy).
- Nebo distribuovaný zámek — kdo první získá zámek na danou úlohu, ten ji spustí, ostatní ji přeskočí.
- A i tak ať je úloha idempotentní — pojistka pro případ, že se i přesto spustí dvakrát.
Failure modes — jak to v praxi praská
- Neidempotentní úloha → opakování pošle dvakrát e-mail nebo dvakrát naúčtuje.
- Cron na víc serverech → naplánovaná úloha se spustí vícekrát naráz.
- Tichá selhání → úloha spadne a nikdo se to nedozví (chybí monitoring a viditelnost stavu).
- Zacyklená vadná úloha → úloha pořád padá a blokuje frontu (proto retry limit + DLQ).
- Worker neuklízí → zpracovaná úloha se neoznačí jako hotová a zpracuje se znovu.
🛠️ Cvičení
- Hned, nebo na pozadí? U těchto úkolů rozhodni: ověření hesla při přihlášení, odeslání uvítacího e-mailu, vygenerování PDF faktury, přepočet doporučení, zápis objednávky. Zdůvodni.
- Idempotentní úloha. Úloha „pošli uvítací e-mail uživateli X" může proběhnout dvakrát. Navrhni, jak zajistíš, že e-mail odejde jen jednou.
- Cron na clusteru. Máš „naúčtuj měsíční platby" a tři servery, na všech běží cron. Co se stane a jak to opravíš?
- Stav úlohy. Navrhni, jak bys uživateli ukázal průběh zpracování nahraného videa (nápověda: stav v databázi + realtime nebo polling).
- Retry + DLQ. Úloha opakovaně padá kvůli vadnému vstupu. Navrhni limit pokusů a co s ní pak.
Náčrt řešení — rozbal, až si cvičení zkusíš sám
- Hned, nebo na pozadí? — ověření hesla = hned (uživatel čeká na výsledek přihlášení), uvítací e-mail = na pozadí (odložitelné, nikdo nečeká), PDF faktura = na pozadí (dlouhé), přepočet doporučení = na pozadí (drahé, není kritické teď), zápis objednávky = hned (uživatel potřebuje potvrzení). Pointa: hned jen to, na čem visí odpověď uživateli. Past: nepřehodit na pozadí krok, jehož výsledek hned potřebuješ.
- Idempotentní úloha — měj příznak/záznam „uvítací e-mail pro uživatele X odeslán" a před odesláním atomicky zkontroluj a označ (
UNIQUE), takže druhý běh ho přeskočí. Pointa: kontrola „už jsem to pro tohle ID udělal?" zařídí, že e-mail odejde jednou. Past: kontrola a označení musí být atomické, jinak dva souběžné běhy projdou oba. - Cron na clusteru — cron běží na všech třech serverech → „naúčtuj platby" proběhne třikrát. Fix: ať úlohy spouští jen leader (leader election), nebo si je servery vybojují distribuovaným zámkem (kdo získá, spustí, ostatní přeskočí). Past: i tak ať je úloha idempotentní jako pojistka, kdyby zámek selhal (TTL vyprší během práce).
- Stav úlohy — ukládej stav v databázi (čeká → běží → hotovo / selhalo) a uživateli ho zobraz buď pollingem (občas se zeptá), nebo elegantněji realtime kanálem, který po dokončení pošle upozornění. Pointa: nikdy ho nenech čekat naslepo. Past: stav aktualizuj i při selhání, ať „běží" nezůstane viset napořád.
- Retry + DLQ — opakuj s backoffem, ale po N marných pokusech zprávu přesuň do DLQ, ať vadný vstup nezablokuje frontu donekonečna; pak ji člověk prohlédne. Pointa: vadný vstup retry neopraví (je to jako
4xx), takže limit pokusů je nutný. Past: alertuj na neprázdnou DLQ, ať selhání není tiché.
🧠 Otázky & odpovědi
Proč odkládat práci na pozadí místo udělat ji rovnou v requestu?
Dlouhá nebo odložitelná práce (e-mail, zpracování souboru) by zbytečně nechala uživatele čekat a při selhání by mu shodila celý request. Když ji odložíš na pozadí, requestu hned odpovíš „přijato" a práci vykoná worker později — uživatel je rychle hotový a případné selhání se dá v klidu opakovat, aniž by o tom uživatel věděl.
Proč musí být úloha na pozadí idempotentní?
Fronty doručují typicky at-least-once a worker může spadnout uprostřed, takže se úloha může spustit vícekrát. Když není idempotentní, znamená to dvakrát odeslaný e-mail nebo dvojí naúčtování. Idempotentní úloha skončí stejně, ať proběhne jednou, nebo třikrát (např. díky kontrole „už jsem to pro tohle ID udělal?").
Jaký je rozdíl mezi úlohou spouštěnou událostí a naplánovanou?
Spouštěná událostí vznikne z nějaké akce (uživatel nahrál soubor → zpracuj ho) a obvykle jde přes frontu. Naplánovaná má běžet v určitý čas, často opakovaně (noční záloha, měsíční faktury), a řeší ji scheduler/cron. Liší se tím, co je spouští: akce vs. čas.
Co se pokazí, když pustíš cron na víc serverech, a jak to vyřešíš?
Naplánovaná úloha se spustí na každém serveru zvlášť → třeba „naúčtuj platby" proběhne třikrát. Řešení: ať úlohy spouští jen jeden server (leader election), nebo ať si o spuštění soupeří přes distribuovaný zámek (kdo ho získá, spustí). A pro jistotu ať je úloha idempotentní.
Jak uživateli ukážeš, že se jeho úloha na pozadí dokončila?
Stav úlohy ukládej (např. v databázi: čeká → běží → hotovo / selhalo). Uživateli ho pak zobrazíš buď pollingem (občas se zeptá na stav), nebo elegantněji realtime kanálem (Realtime), kterým mu po dokončení rovnou pošleš upozornění. Nikdy ho nenech jen „čekat naslepo".
