Cron presentery#
Naplánovaná úloha je v tomhle CMS presenter jako každý jiný — spouští se HTTP
požadavkem s tokenem a vrací JSON. Kód: app/UI/Cron/, základ
app/Core/Base/Cron/BasePresenter.php.
Dnes je to 59 akcí v 9 modulech (grep -rn "public function action" app/UI/Cron
--include="*Presenter.php" | wc -l). Jejich seznam se do manuálu
generuje z kódu.
Co base zařizuje sám#
| Krok | Co se stane |
|---|---|
initializeSetting() |
načte nastavení (kvůli tokenu) |
verifyToken() |
porovná ?token= s 1/cronToken přes hash_equals(); neshoda → 403 |
| zámek souběhu | INSERT IGNORE do cms_cron_locks pod jménem <presenter>.<akce> |
shutdown() |
zámek uvolní |
K tomu dvě metody odpovědi: sendSuccess($data) → {"status":"ok", …}
a sendError($message, $code) → {"status":"error", …}.
Prázdný cronToken v nastavení zamkne VŠECHNY úlohy
verifyToken() odmítne požadavek i tehdy, když je očekávaný token prázdný — jinak
by prázdné nastavení znamenalo veřejné endpointy. Na novém prostředí je proto
potřeba token nastavit dřív, než se crony zapojí; jinak vrací 403 a vypadá to jako
špatná adresa.
Token se posílá v query stringu, tedy se loguje
Objeví se v access logu webserveru a v historii shellu. Neber ho jako tajemství srovnatelné s heslem — je to ochrana proti náhodnému spuštění, ne proti útočníkovi s přístupem k logům.
Postup: zakládám novou úlohu#
- Presenter do
app/UI/Cron/<Modul>/Presenters/XxxPresenter.php,extends App\UI\Cron\BasePresenter, v konstruktoru jen manager. - Akce je pár řádků — zavolat manager a poslat výsledek:
/**
* Přepočítá denormalizovaný sloupec price u všech inzerátů.
*
* Spouštět periodicky, např. každých 10–15 minut.
*
* URL: /cron/bazaar/advert/recount-prices?token=xxx
*/
public function actionRecountPrices(): void
{
$this->sendSuccess($this->advertManager->recountPrices());
}
- Do docblocku napiš
Frekvence:neboSpouštět— z toho se generuje sloupec „Jak často“ v manuálu. Bez toho se při sestavení manuálu ozve varování. - Do docblocku napiš i
URL:— generátor z něj bere adresu. - U neperiodické úlohy napiš rovnou „ručně“; vymýšlet jí cron výraz je horší než nic.
- Doplň úlohu do checklistu nasazení — generátor jinak při sestavení manuálu ohlásí, že v plánu chybí.
- Ověř spuštěním — se správným tokenem i bez něj.
Logika v cron presenteru se nedá zavolat odjinud ani otestovat
Cron presenter injektuje jen managery — nikdy service vrstvu, generátory nebo
EntityManagerInterface. Co je v presenteru, existuje jen pro cron; totéž pak
potřebuje admin a napíše se to podruhé.
Souběh#
Zámek je řádek v cms_cron_locks pojmenovaný <presenter>.<akce>. Získává se
atomicky INSERT IGNORE; předtím se smaže zámek po vypršení TTL (výchozí 300 s),
aby pád bez uvolnění nezablokoval úlohu navždy. Druhá instance dostane
{"status":"skipped","reason":"already_running"}.
Zámek je fail-open
Když tabulka neexistuje nebo dotaz selže, acquire() vrátí true a úloha běží.
Je to záměr — zámek nesmí zablokovat cron — ale znamená to, že rozbitá tabulka
zámků vypadá jako by ochrana fungovala, a přitom neexistuje.
Dlouhá úloha zablokuje ta následující spuštění
Běží-li reindex hodinu a plánovač je nastavený na deset minut, pět spuštění se
zahodí jako skipped. Frekvenci volte podle skutečné doby běhu, ne podle toho,
jak často by se to hodilo.
TTL 300 s je kratší než leckterá úloha
Když úloha běží déle než pět minut, může jí zámek vypršet pod rukama a druhá instance se rozběhne souběžně. U dlouhých úloh proto předej vlastní, delší TTL.
Dávky#
Úlohy zpracovávají po dávkách; velikost je v nastavení modulu (7/paymentBatchSize,
7/scheduleBatchSize, …), typicky 10.
Menší dávka častěji je lepší než velká zřídka
Přerušení uprostřed velké dávky znamená, že se část práce udělá dvakrát — a u idempotentních operací to nevadí, u těch ostatních ano.
Dva druhy chyby#
Zpracování položky může selhat dvěma způsoby a rozlišit je je nutné:
| Druh | Chování | Příklad |
|---|---|---|
| přechodná | příznak odbavení se neposune, položka se zkusí příště | chyba uložení, dočasně nedostupná služba |
| trvalá | položka se odbaví bez výsledku a zapíše do chyb k ručnímu vyřešení | osiřelý odkaz, chybějící zdrojový záznam |
Bez rozlišení jedna vadná položka vyhladoví celou frontu
Trvale chybná položka, která se zkouší donekonečna, ukrajuje z dávky každý běh a ostatní se nikdy nedostanou na řadu. Proto se odbaví s poznámkou a jde se dál — viz Fakturace, oddíl o automatice.
Odbavená trvalá chyba znamená, že se výsledek nikdy neudělá
Zaplacená objednávka bez faktury nikoho neupozorní. Přehled chyb se musí kontrolovat, jinak je „úloha proběhla v pořádku“ zavádějící.
Identita a oprávnění#
Cron obchází RBAC guard na interní cestě
Injektuje Api managery přímo, ne přes ApiBridge — takže internalMode se ho
netýká. Změřeno: nula řádků API_RBAC path=internal za šest běhů.
CLI skript přes ApiBridge běží jako guest
To je něco jiného než cron. Skript bez session ve vynucujícím režimu nedostane data
a vrátí tiché prázdno, ne chybu. Musí si identitu dodat sám:
ApiBridge::setInternalAuthToken($jwtManager->generateToken(…)). Podrobně
RBAC v API.
Pole se v cronu NEfiltrují podle publika
Mimo požadavek není žádný volající, takže #[FieldAccess] se neuplatní. Je to
záměr — aktivační e-mail potřebuje activationKey — ale znamená to, že co cron
pošle ven, si musí ohlídat sám.
Kam sáhnout#
| Chci | Kde |
|---|---|
| co base zařizuje | app/Core/Base/Cron/BasePresenter.php |
| zámek souběhu | app/UI/Api/System/Models/Managers/CronLockManager.php |
| existující úlohy | app/UI/Cron/<Modul>/Presenters/ |
| seznam všech úloh | Naplánované úlohy |
| generátor toho seznamu | manual/tools/gen-cron-list.py |
Navazující kapitoly: Naplánované úlohy · Checklist nasazení · RBAC v API · Konvence kódu