Files
csbot-prototype/documentation/20-fronta-a-runtime.md
T
JiriUhlirandClaude Opus 5 a57eca123e Fronta a worker: webhook odpovi hned, praci udelaji workeri
Webhook uz nic nevykonava v requestu. Zapise udalost do fronty a odpovi 202
do jednotek milisekund; strom vykona worker na pozadi. Za konektory nerucime,
takze cekat na cizi sluzbu v requestu znamena ztracet udalosti pri timeoutu.

Fronta ma opakovani s rostouci prodlevou (30 s, 2 min, 10 min, hodina),
spravedlive poradi po firmach (jedna firma s tisicem udalosti nezablokuje
ostatni), navrat zaseknutych behu po restartu a uklid hotovych. Marna chyba
se neopakuje - chybejici skript za minutu existovat nezacne.

Tri druhy spoustecu: push (webhook), vnitrni udalost (vznik a zmena ticketu)
a pull, tedy pravidelne dotazovani u sluzeb bez webhooku (posta, zpravy).
Planovac jen rekne "je cas", samotny dotaz je prvni krok stromu, takze ma
zaznam v logu a opakuje se pri chybe jako cokoliv jineho.

Kontrakt tela webhooku: kazdy parametr ma cestu (data.order.id,
errors.0.message), takze jde napojit i odesilatel s vnorenym modelem.
U adresy je metoda, ukazka tela a kopiruje se cela adresa vcetne domeny.

Vnitrni kroky, ktere sahaji do naseho uloziste: ticket/upsert (zaloz nebo
dopln podle externiho ID), assign-least-busy, assign-by-external, set-type,
set-stage, add-tags, set-status, incident/create, flow/pause a flow/log.

Faze ticketu jako treti osa vedle stavu a stitku. Stav je zivotni cyklus
a pocitaji se z nej statistiky, faze je workflow daneho typu a muze byt jen
jedna, takze se na ni da spolehnout v podmince.

ID z cizich aplikaci u resitele: voicebot posle voicebotId a ticket skonci
u toho, komu patri. Vazba je na jednom miste, ne v kazde automatizaci.

Kazda chyba zaklada incident se dvema urovnemi: impact cte klient a je
srozumitelny, detail cte admin a je v nem cely beh, ktery krok selhal, cele
hlaseni a data na vstupu. Detail vidi jen spravce platformy.

Ochrana proti smycce: automatizace navazana na zmenu ticketu ticket meni,
cimz se spousti znovu - pri vyvoji to server polozilo. Resi to oznaceni behu
pres AsyncLocalStorage a strop peti behu na jeden ticket za minutu.

Upozorneni pri prideleni prace vcetne cisla u zalozky Tickety. Zivy dashboard:
dlazdice nad nasimi daty na udalost, data z konektoru podle ttlSec s moznosti
vynutit nacteni znovu.

Opraveno: path a intervalSec u spoustece se pri ulozeni zahazovaly; nad
seznamem neslo pouzit contains, takze na stitky neslo postavit podminku;
novejsi vystup kroku ted prekryje starsi misto hlaseni konfliktu.

Overeno dvema scenari proti bezicimu serveru, 34 kontrol: firma se skladem,
expedici a IT, a hovory z voicebota (callSid do externiho ID, status do faze,
prirazeni podle voicebotId, tri zpravy = jeden ticket se tremi udalostmi).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 16:41:02 +02:00

8.7 KiB

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, model ticketu v 18-ticketovaci-system.md.

Webhook odpoví hned, práci udělá worker

POST /webhook/<token>
  -> 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:

{
  "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/set-stage posune do další fáze workflow daného typu
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 pevná čtveřice (nový, v řešení, čeká, vyřešeno) životní cyklus, počítají se z něj statistiky a fronta
stage firma u typu ticketu (TicketType.statuses) postup uvnitř typu: čeká na zabalení, předáno dopravci
tags kdokoliv, volně označení, která spolu nemusí souviset

Fáze může být jen jedna, proto se na ni 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 mimo workflow typu se odmítne. Překlep by jinak tiše vyřadil podmínku, která na fázi stojí.

Ž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.