# Fronta, worker a spouštěče Jak se událost dostane od webhooku k vykonanému stromu. Rozbor kapacity je v [19-kapacita-200-firem.md](19-kapacita-200-firem.md), model ticketu v [18-ticketovaci-system.md](18-ticketovaci-system.md). ## Webhook odpoví hned, práci udělá worker ``` POST /webhook/ -> kontrola těla podle kontraktu -> zápis do fronty -> 202 Accepted (do jednotek milisekund) worker (na pozadí) -> vezme z fronty -> vykoná strom -> zapíše výsledek do logu ticketu -> při chybě naplánuje další pokus, nebo založí incident ``` Odesílatel **nikdy nečeká** na cizí službu. Důvody: - Za konektory neručíme. iDoklad může odpovídat pět sekund nebo být hodinu mimo. Kdyby se čekalo v requestu, odesílateli vyprší timeout a událost je pryč, přestože jsme ji dostali. - Pád procesu nesmí ztratit práci. Záznam ve frontě restart přežije, rozdělaný běh v paměti ne. - Bez fronty není kam si poznamenat, že se to má za minutu zkusit znovu. Odpověď 202 znamená "převzali jsme to", ne "hotovo". Výsledek se hledá v `GET /api/dashboard/runs` nebo v logu ticketu. ## Co frontu plní | Druh | Kdo to spustí | Příklad | | --------------- | ---------------------- | ----------------------------- | | Push | cizí služba zavolá nás | e-shop pošle novou objednávku | | Vnitřní událost | něco se stalo u nás | vznikl nebo se změnil ticket | | Pull | ptáme se sami | e-mail, zprávy z Messengeru | ### Pull, tedy pravidelné dotazování Většina služeb webhooky nemá. U e-mailu a schránek zpráv se **musíme ptát**. Dělá to plánovač: každých 30 sekund projde automatizace, jejichž spouštěč je "musí se obvolávat", a u těch, kterým uplynula perioda, zařadí běh. Plánovač **sám nic nevolá**. Jen řekne "je čas" a samotný dotaz je první krok stromu. Díky tomu se dotazování chová stejně jako cokoliv jiného: má záznam v logu, opakuje se při chybě a jde ho změnit bez zásahu do kódu. Výchozí periody: pošta 60 s, zprávy 30 s, plánovač 60 s. Automatizace si to může přepsat polem `intervalSec` u spouštěče, minimum je 10 sekund - kratší už není dotazování, ale útok na cizí službu. Jeden čekající dotaz na automatizaci: když předchozí ještě běží, další se nezařadí. Jinak by se fronta zaplnila dotazy na službu, která stejně nestíhá. ## Kontrakt webhooku Odesílatelé posílají různé tvary. Jeden `{"a":"aaa"}`, druhý celý model s vnořenými objekty a poli. Proto má každý parametr spouštěče **cestu**: ```json { "document": { "id": "D-99" }, "errors": [{ "code": "OCR_FAIL", "message": "Nepodařilo se přečíst částku" }] } ``` | Parametr | Cesta | Typ | | -------------- | ------------------ | ------ | | `docId` | `document.id` | string | | `errorMessage` | `errors.0.message` | string | Ve stromu se pak píše `{{docId}}` bez ohledu na to, jak hluboko to odesílatel schoval. Celé tělo je navíc pod `_body`, takže se nic neztratí. Chybějící povinný parametr vrací 400 s tím, který to je a kde se hledal. Přebytek v těle nevadí - odesílatel často posílá víc, než potřebujeme, a odmítnout ho kvůli tomu by znamenalo, že webhook nejde zapojit. `GET` na tutéž adresu vrátí nápovědu: co se čeká, na jakých cestách a ukázku těla. V portálu je u adresy vidět totéž včetně metody, a kopíruje se **celá adresa včetně domény**. ## Opakování a vzdání se | Pokus | Kdy | | ----- | --------- | | 1. | hned | | 2. | za 30 s | | 3. | za 2 min | | 4. | za 10 min | | 5. | za hodinu | Pak běh skončí jako `failed` a zůstane k nahlédnutí. Nemaže se: bez záznamu by nikdo nezjistil, že se něco nestalo. **Marná chyba se neopakuje vůbec.** Chybějící skript nebo neexistující skupina za minutu existovat nezačne, takže se běh rovnou vzdá. Opakuje se jen to, co může pominout: nedostupná služba, timeout. ## Incident z chyby Každá chyba, kterou už nemá smysl zkoušet, založí incident se **dvěma úrovněmi**: - `title` a `impact` čte **klient**. Bez názvů kroků a ID běhů: "Automatizace u ticketu TK-4822 nedoběhla do konce, data jsme neztratili." - `detail` čte **admin**. Je v něm všechno: která automatizace, který běh, kolik pokusů, který krok selhal, celé hlášení, výpis všech kroků a data, která přišla na vstupu. `detail` se vrací **jen správci platformy**. Je to naše diagnostika, ne informace pro zákazníka. Stejná příčina nezakládá druhý incident, dokud je první otevřený. Jinak by deset stejných chyb znamenalo deset incidentů a nikdo by se v tom nevyznal. ## Ochrana proti smyčce Automatizace navázaná na změnu ticketu ticket změní, čímž se spustí znovu. Bez ochrany to server položí, což se při vývoji stalo. Dvě pojistky: 1. **Označení běhu.** Změna, kterou udělala automatizace X, nespustí automatizaci X. Používá se na to `AsyncLocalStorage`, protože běží čtyři běhy naráz a obyčejná proměnná by patřila všem. 2. **Strop na ticket.** Jedna automatizace smí nad jedním ticketem běžet nejvýš pětkrát za minutu. Chytí to i smyčku mezi dvěma automatizacemi, kterou první pojistka nepozná. Překročení se zaloguje, aby to šlo spravit. ## Spravedlivost mezi firmami Z každé firmy se na jedno kolo vezme nejvýš jeden běh. Jedna firma s tisícem událostí tak nezablokuje ostatní - bez toho stačí jeden rozbitý e-shop a servicedesk stojí všem. ## Kroky, které děláme my Založit ticket nebo přehodit ho na člověka není volání cizí služby, takže to nejde přes skript - sahá to do našeho úložiště. Pro uživatele je to v katalogu operace jako každá jiná. | Krok | Co dělá | | -------------------------- | --------------------------------------------------------------------- | | `ticket/upsert` | podle externího ID založí ticket, nebo na existující navěsí událost | | `ticket/assign-least-busy` | předá nejvolnějšímu ze skupiny, při shodě rozhoduje podíl ke kapacitě | | `ticket/set-type` | nastaví typ, za kterým stojí vlastní pole | | `ticket/add-tags` | přidá štítky, existující nechá | | `ticket/set-status` | změní stav v životním cyklu | | `incident/create` | založí incident | | `flow/pause`, `flow/log` | pauza a zápis do logu | ## Tři osy na ticketu | Osa | Kdo ji určuje | K čemu | | -------- | ---------------------------------------------- | -------------------------------------------------------- | | `status` | odesílatel, nabídku dává `TicketType.statuses` | kde ticket je: čeká na zabalení, předáno dopravci | | `closed` | výslovně, krok nebo člověk | co už nikdo neřeší, z toho se počítá fronta a statistiky | | `tags` | kdokoliv, volně | označení, která spolu nemusí souviset | Stav může být **jen jeden**, proto se na něj dá spolehnout v podmínce. Přes štítky by to fungovalo taky, ale ticket by mohl mít "čeká na zabalení" i "expedováno" naráz a nikdo by nepoznal, co platí. Fáze jako třetí osa tady byla a **je zrušená**. Když je stav volný řetězec, je druhé pole na tutéž věc jen zmatení. V katalogu po ní zbýval krok "Posunout do další fáze" a pole Fáze u založení ticketu, jenže model fázi neměl - krok neměl co vykonat a pole se tiše zahazovalo. ## Živý dashboard Dlaždice nad našimi daty se překreslí na událost ze streamu, tedy hned. Data z konektorů ne: server je drží v mezipaměti podle `ttlSec` u widgetu. Přehled se po uplynutí té doby zeptá znovu a dokud je mezipaměť čerstvá, dostane ji zpátky bez volání cizí služby. U dlaždice je vidět stáří dat a tlačítko, které vynutí načtení znovu. Bez mezipaměti by otevření přehledu znamenalo volání cizího API za každou dlaždici, a to má limity a někdy se za to platí. ## Ověřený scénář Scénář jedné firmy: sklad (3 lidi), expedice (2), IT (3), typy ticketu objednávka a chyba, dvě automatizace. 1. Web pošle `{"kind":"order.created","data":{"order":{"id":"5001"}}}`. 2. Webhook odpoví **202 za 12 ms**, nic nečeká. 3. Worker založí ticket, dá mu typ objednávka a štítek čeká na zabalení. 4. Změna ticketu spustí druhou automatizaci, ta podle typu a štítku předá práci **nejvolnějšímu ze skladu**. 5. Druhá objednávka jde **jinému člověku**, protože první už jednu má. 6. Chyba z převodníku dokladů přijde s vnořenou cestou `errors.0.message`, vznikne ticket typu chyba, dostane ho IT a **založí se incident**. Ověřeno 19 kontrolami proti běžícímu serveru. ## Co zbývá - **Víc instancí.** Výběr z fronty je v paměti jednoho procesu. Nad Postgresem to musí být `SELECT ... FOR UPDATE SKIP LOCKED`, jinak si dva workery vezmou tentýž běh. Místo je označené v `runtime/queue.ts`. - **Strop souběžných volání na dvojici firma a služba** a vypnutí služby po sérii chyb. Timeout a rozlišení "zkusit znovu / marné" už ve `scripts/http.ts` je. - **Dlouhé čekání** (pošli e-mail za tři dny) přes běh naplánovaný na později. Krok `flow/pause` umí nejvýš minutu, protože blokuje běh.