Jak nerozbít produkci, když se zdroj dat mění sám
Patch-to-production pipeline: externí data validovaná kontraktem v CI, atomický swap symlinku a rollback bez re-extrakce. Vzor pro data mimo tvou kontrolu.
Máš aplikaci, která žije z dat, jež nevlastníš. Cenový feed, externí API se schématem, které se občas změní bez varování, katalog třetí strany. Dodavatel vydá novou verzi, kdy se mu zachce - tvůj release cyklus ho nezajímá. A ty potřebuješ ta data dostat do produkce bez výpadku a bez rizika, že nasadíš něco rozbitého.
Přesně tenhle problém řeším v exile2exile, open-source nástroji pro Path of Exile 2. Herní patch znamená nová data: strom pasivních dovedností, itemy, gemy, tisíce ikon. Všechno se extrahuje přímo z oficiálního patch serveru a aplikace nad nimi staví. Hra se patchuje klidně dvakrát týdně, takže "ručně to nasadím, až budu u počítače" nepřipadá v úvahu. PoE2 je tu ale jen konkrétní příklad - vzor, který popíšu, sedí na cokoliv, co "vydává patche" mimo tvou kontrolu.
Proč nestačí cron a přepsat soubory
Naivní řešení se nabízí samo: cron jednou za hodinu stáhne nová data a přepíše ty stará na disku. Jenže tím si koleduješ hned o tři průšvihy najednou:
- Half-switched state. Přepisuješ tisíce souborů a mezitím chodí requesty. Uživatel dostane strom z nové verze a ikony ze staré. U velkých dat trvá přepis minuty, ne milisekundy.
- Rozbitá data jdou rovnou do produkce. Dodavatel změnil strukturu, přejmenoval pole, půlka ikon chybí? Zjistíš to z error logů, až ti to uživatelé rozbijou.
- Není cesta zpět. Stará data jsi právě přepsal. Rollback znamená znovu stáhnout a znovu zpracovat starou verzi - pokud ji dodavatel vůbec ještě nabízí.
Pipeline, která tohle řeší, má pět částí: detekci, izolovanou extrakci, kontrakt v CI, atomický swap a defaultní failure mode "žádná změna".
Detekce: watcher, ne webhook
GGG (vydavatel PoE2) žádné webhooky neposílá, takže zbývá polling: příkaz poe2:watch-patch se každých 5 minut zeptá patch serveru na aktuální verzi. Nová verze spustí dvě věci: notifikaci (Discord + webhooky pro odběratele) a extrakční job.
U cenového feedu nebo API to bude vypadat stejně - když dodavatel neumí push, polluj a porovnávej verzi nebo checksum. Důležité je, že detekce jen spouští pipeline, nikdy sama nemění živá data.
Izolovaná extrakce: nová verze vedle, ne přes
Extrakce zapíše novou verzi do releases/<version> - vedle živých dat, ne přes ně. Produkce celou dobu běží na staré verzi a o probíhající extrakci neví.
Samotná extrakce z herního formátu GGPK žije v poe2-toolkit, oddělené vrstvě MIT npm balíčků. Toolkit je čistě kód, žádná data - umí z patch serveru vytáhnout strom, itemy a ikony, ale nic neví o tom, jak je aplikace používá. Tohle oddělení se vyplácí: extraktor se dá testovat a verzovat samostatně a aplikace řeší jen "mám adresář s daty ve známé struktuře".
Po extrakci aplikace release zabalí do tarballu, spočítá sha256 a nastaguje ho ke stažení.
Kontrakt, ne důvěra
Tady je jádro celého vzoru. Nasadit data jen proto, že se povedlo je stáhnout a rozbalit, je důvěra. Já chci kontrakt: sadu testů, která ověří, že data mají přesně tvar, jaký aplikace očekává.
Aplikace po nastagování release dispatchne GitHub Actions workflow data-contract.yml a předá mu verzi a sha256. CI pak:
- stáhne tarball z aplikace a ověří checksum - má jistotu, že testuje přesně ty bajty, které watcher nastagoval, i kdyby mezitím vyšel další patch,
- rozbalí ho do checkoutu a pustí Contract suite (
tests/Contract) - reálné Pest testy nad reálnými daty: struktura stromu, všechny odkazované ikony existují na disku, import z Path of Building projde end-to-end, seedované plány se načtou, - na zelenou zavolá aktivační endpoint aplikace.
Klíčový detail: CI nikdy nesahá na zdrojová data dodavatele. Neextrahuje nic z GGPK - validuje přesně ten artefakt, který server nastagoval a který půjde do produkce. Build once, promote. Release, který jde live, je bajt po bajtu ten, co prošel testy.
Checksum má v pipeline ještě jednu praktickou roli: je součástí cache klíče, pod kterým si CI ukládá stažený tarball. Toho využívám při ručním spouštění - když workflow dispatchnu se stejnou verzí, ale záměrně jiným checksumem, cache se mine, tarball se stáhne znovu a Contract testy proběhnou čerstvě od nuly. Jednoduchý způsob, jak si vynutit nový běh testů bez čekání na další patch.
A protože kontrakt má dvě strany, běží stejná suite i při každém push do main - tentokrát proti verzi, kterou produkce právě používá. Změna kódu, která rozbije kontrakt s daty, tak neprojde zeleně, stejně jako data, která rozbijí kontrakt s kódem.
GGG patch server Produkce GitHub Actions
| | |
|<-- poll (à 5 min) ---| |
|--- nová verze ------>| |
| |-- extrakce do releases/<v> |
| | (živá data netknutá) |
| |-- tarball + sha256 |
| |-- dispatch (verze, sha256) ->|
| |<------ stáhne tarball -------|
| | ověří checksum |
| | Contract suite |
| | |
| zelená: |<--- POST /api/data/activate -|
| |-- atomický swap symlinku |
| červená: | žádný swap, jede
| | poslední ověřená verze
Atomický swap: jeden rename
Aplikace čte všechna herní data přes jediný symlink: storage/game-data/current -> releases/<version>. Aktivace nové verze je jeden atomický rename symlinku.
Proč na tom záleží: rename je na POSIX filesystémech atomická operace. Request, který přišel nanosekundu před swapem, dočte celou odpověď ze staré verze; request nanosekundu po něm čte celou z nové. Neexistuje okamžik, kdy by kdokoliv viděl mix obou - ten half-switched state z naivního řešení prostě nemůže nastat. Žádný maintenance mode, žádný restart, žádný výpadek.
Endpoint POST /api/data/activate je chráněný tokenem a idempotentní - opakované volání se stejnou verzí nic nerozbije.
Selhání = žádná změna
Nejcennější vlastnost celé pipeline je její defaultní failure mode. Červené testy? Žádný swap. Tarball se nepodařilo stáhnout? Žádný swap. Server nedostupný, checksum nesedí, workflow spadl v půlce? Žádný swap. Každá cesta selhání končí stejně: produkce jede dál na poslední ověřené verzi.
To je přesný opak cron řešení, kde selhání v půlce přepisu znamená rozbitou produkci. Tady se o nasazení nemusí nikdo aktivně starat - špatná verze se do produkce nedostane, dokud ji někdo (typicky nový patch s opravenými daty) nenahradí zelenou.
Watcher navíc hlídá zaseknuté validace: když staged verze dlouho čeká, re-dispatchne workflow, ale nejvýš jednou za 6 hodin. Bez limitu by nedostupné CI znamenalo dispatch každých 5 minut donekonečna.
Rollback bez re-extrakce
Staré release zůstávají na disku (starší se průběžně promazávají, pár posledních se drží). Rollback je tak stejné jedno volání POST /api/data/activate se starší verzí - jeden rename symlinku, hotovo za milisekundy. Žádné stahování, žádná re-extrakce, žádná závislost na tom, jestli dodavatel starou verzi ještě nabízí.
Když dodavatel vydá vadný patch a opraví ho až za den, přepneš zpátky a den v klidu počkáš.
Kde jinde se to hodí
Vzor stojí na čtyřech nohách a žádná z nich není specifická pro herní data:
- verzované release vedle sebe, živá data se nikdy nepřepisují,
- kontrakt v testech, který validuje přesný artefakt určený k nasazení,
- atomické přepnutí přes symlink (nebo pointer v DB, alias v S3, traffic switch),
- selhání nechává poslední ověřenou verzi.
Sedne všude, kde tě externí zdroj může rozbít nezávisle na tvém release cyklu: cenové a kurzovní feedy, katalogy produktů od dodavatelů, ML modely a jejich váhy, geodata, číselníky ze státních registrů, API třetích stran se schématem, které se "nemění" (dokud se nezmění). Pointa je vždycky stejná: nedůvěřuj datům, která jsi nevyrobil. Dej jim kontrakt a nech je ho podepsat v CI, dřív než se dotknou produkce.
Celá implementace je open source: github.com/rajtik76/exile2exile (pipeline, Contract suite, watcher) a github.com/rajtik76/poe2-toolkit (extrakce dat).