Background Jobs

Workery, fronty úloh, scheduling, idempotentní joby.

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í

  1. 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.
  2. 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.
  3. 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íš?
  4. 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).
  5. 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
  1. 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š.
  2. 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.
  3. 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).
  4. 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.
  5. 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".