Přeskočit obsah
V
Pro vývojáře
Architektura, konvence, jádro systému a bezpečnost
UI vrstva / Cron presentery

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.

/cron/<modul>/<presenter>/<akce>?token=xxx

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#

  1. Presenter do app/UI/Cron/<Modul>/Presenters/XxxPresenter.php, extends App\UI\Cron\BasePresenter, v konstruktoru jen manager.
  2. 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());
}
  1. Do docblocku napiš Frekvence: nebo Spouštět — z toho se generuje sloupec „Jak často“ v manuálu. Bez toho se při sestavení manuálu ozve varování.
  2. Do docblocku napiš i URL: — generátor z něj bere adresu.
  3. U neperiodické úlohy napiš rovnou „ručně“; vymýšlet jí cron výraz je horší než nic.
  4. Doplň úlohu do checklistu nasazení — generátor jinak při sestavení manuálu ohlásí, že v plánu chybí.
  5. 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