Skip to content

feat(warmup): drain the tail the cap excluded, a batch at a time - #138

Open
parisek wants to merge 14 commits into
mainfrom
feat/breeze-warmup-tail
Open

feat(warmup): drain the tail the cap excluded, a batch at a time#138
parisek wants to merge 14 commits into
mainfrom
feat/breeze-warmup-tail

Conversation

@parisek

@parisek parisek commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Prioritizace (#134) nahřeje po purgi prvních 200 URL v pořadí podle důležitosti. Všechno za stropem zůstane studené, dokud si o to někdo neřekne. Tenhle PR ten zbytek dohřívá — pomalu, po dávkách, ve stejném pořadí, a jen když má origin volno.

Uzavírá #136.

Co tenhle návrh záměrně NESLIBUJE

Dohřívání nemá stav „hotovo".

Plný purge přichází na těchhle projektech i několikrát denně. Ocas o 2 800 URL by při tempu Breeze (~1 s/URL) trval nejméně 47 minut souvislé zátěže, takže každý další purge ho shodí dřív, než doběhne. Slib „po purgi bude celý web teplý" by byl nesplnitelný — a zapnutý flag, který takový slib budí, je horší než žádná funkce.

Místo toho: ber další v pořadí, dokud tě někdo nezastaví. Ať se stihne dvě stě URL nebo dva tisíce, vždycky to byly ty nejcennější, které se zrovna stihly. Na rušném webu se pokryje třeba 40 % sitemapy — zato pořád těch správných 40 %.

Jak to funguje

Cron obnova nově uloží i ocas — jen URL, seřazené stejným Scorer::sort() jako zbytek — do vlastního option řádku, který horká cesta nikdy nečte. Purge znuluje kurzor a zařadí první tik. Každý tik odebere dávku od kurzoru, pošle ji do Breeze_Cache_Preloader::preload_url() a zařadí nástupce.

TIK (Action Scheduler, každých 5 minut)
  ├─ modul nebo flag vypnutý?         → konec
  ├─ fronta Breeze neprázdná?         → přeskoč, zařaď nástupce
  ├─ ocas prázdný?                    → konec
  ├─ otisk se neshoduje?              → kurzor na nulu
  ├─ index >= délka ocasu?            → konec
  ├─ vezmi dávku, odešli ji
  ├─ posuň kurzor (podmíněným zápisem)
  └─ zařaď nástupce

Řetěz se sám ukončí. Když ocas dojde, další tik se nezařadí — žádný cron běžící naprázdno.

Brzda: fronta Breeze

get_option( 'breeze_preload_queue' ) není prázdná → tik přeskoč

Měří přesně tu správnou věc: běží už teď nahřívání? Load average by řekl jen „je rušno" a nerozlišil by, jestli za to můžou návštěvníci, záloha, nebo naše vlastní nahřívání.

Vedlejší efekt, který stojí za pojmenování: vzniká přirozené pořadí. Nejdřív doběhne Breeze svých 200 nejdůležitějších, teprve pak se rozjede ocas — bez jediné synchronizační konstrukce, jen proto, že se dohřívač ptá, jestli má uvolněno.

Přiznané okno: Breeze si dávku z fronty odebere před jejím odesláním, takže u poslední dávky vidí brzda volno, ačkoli tři URL jsou ještě ve vzduchu. Trvá to zhruba sekundu na konci tříapůlminutového běhu; vědomě to přijímáme.

Zapnutí

Vypnuté ve výchozím stavu. Vyžaduje oba předchozí flagy — bez pořadí není co dohřívat.

protected bool $breeze_warmup_sitemap  = true;
protected bool $breeze_warmup_priority = true;
protected bool $breeze_warmup_tail     = true;
protected int  $breeze_warmup_tail_batch = 100;   // URL na 5minutový tik

Interval je pevných 5 minut, dávka je jediný knoflík: „kolik renderů originu za pět minut mi nevadí". Filtry timberkit_warmup_tail_batch a timberkit_warmup_tail_max_urls (výchozí strop ocasu 5000).

Pozor na aritmetiku: 100 na tik nevychází na 1200 za hodinu, protože tik ustupuje frontě Breeze. Skutečná propustnost je nižší.

Souběh

Kurzor má dva zapisovatele — purge ho nuluje, tik posouvá. Tik, který přečetl index 500 a mezitím mu purge znuloval kurzor, nesmí zapsat 600: tím by reset zrušil a nechal URL pod pětistovkou nenahřáté za kurzorem, který tvrdí, že hotové jsou. Řeší to podmíněný zápis, stejný optimistický zámek jako PriorityStore.

Docblock TailStore říká i to, co guard nechrání: dva skutečně souběžné tiky můžou oba projít kontrolou. Vzácné (řetěz pohání jediný plánovaný tik) a neškodné — opakovaná dávka je pár requestů navíc, ne poškozený stav.

Studený start

Ocas plní odložená obnova, ale tik zařazuje purge. Při prvním purgi na projektu může tik doběhnout dřív než obnova, uvidět prázdný ocas a skončit — a obnova pak zapíše ocas, který nic nespustí. Proto obnova po úspěšném zápisu neprázdného ocasu zařadí tik sama.

Multisite

Nedrátuje se tam. Breeze scopuje breeze_preload_queue per blog, takže by brzda vždy hlásila volno a dohřívání by sypalo do originu bez omezení na každém webu sítě. Odmítnout se zaregistrovat je bezpečnější než běžet bez brzdy.

Prioritizace na multisite funguje dál beze změny — vypíná se jen dohřívání ocasu.

Ověření

Brána Stav
composer test 1675 testů, 1 skipped, 0 selhání, 0 risky
composer test:property 20 testů (invariant: průchod ocasem navštíví každou URL právě jednou)
composer phpstan bez chyb
composer adr 6 ADR, všechny v indexu

Dvě nové čisté třídy v src/Breeze/TailPlanner (rozdělení, otisk, dávka) a TailStore (dva option řádky, podmíněný zápis). Obě bez jediného volání WordPressu, takže property test splňuje izolační konvenci z AGENTS.md.

Žádné ADR — je to důsledek rozhodnutí zapsaného v ADR-0006, ne nové architektonické rozhodnutí.

Známá omezení

  • Kurzor počítá odeslané, ne nahřáté. preload_url() vrací void a URL může odmítnout. Vyčerpaný kurzor neznamená nahřátý ocas.
  • Dávka se čte jednou, při registraci — stejně jako váhy prioritizace.
  • Measure whether warmup requests reach the origin behind the Cloudways CDN #135 zůstává otevřené. Než se tenhle flag zapne na projektu s Cloudways CDN, je potřeba změřit, jestli tam warmup requesty vůbec dorazí na origin. Není to vada kódu, je to kontrola při nasazení.

parisek and others added 14 commits August 24, 2026 18:50
Breeze scopes breeze_preload_queue per blog on multisite, so the tail
tick's brake would always read idle there and the drain would pile
onto the origin unthrottled. register() now detects is_multisite()
and leaves the tail hooks unwired instead of running without a
working brake. Also documents the cold-start rescue and that the
batch size, like the priority weights, is read once at registration.
… alone

Four tests relied solely on Functions\expect(...)->never(), a Mockery
expectation verified at teardown, not a PHPUnit assertion. That left
zero assertions and marked them risky, and made them read as tests
that prove absence by proving nothing. Capture the calls with
Functions\when(...)->alias(...) and assert the resulting array is
empty instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1sCgqDJKj7dGQrg5c25bY
@parisek parisek self-assigned this Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant