Dvojí stisk Publikovat teď publikuje jen jednou
Tři žádosti o publikaci stejné aplikace dorazily do našeho API po 84 milisekundách a každá spustila celé nasazení té aplikace. Závodily o stejný hosting, několik výsledných vydání selhalo, protože jiné už přišlo první, a jediným viditelným znamením byl e-mail „vydání selhalo“ – aplikace sama nakonec fungovala. Teď publikace, která přijde, zatímco pro stejnou aplikaci běží jiná, nespustí druhé sestavení. O tom, která žádost vyhraje, rozhoduje databáze a ostatní dostanou pravdivou odpověď: publikace už běží.
Co se stalo
Když publikuješ aplikaci na Mythexu, API zapíše záznam nasazení a požádá náš orchestrátor – službu, která spouští sestavení – aby ji sestavil a vydal. Publikovat lze z tlačítka Publikovat, z jiné karty prohlížeče i z agenta, když o to požádáš. Dvojklik, dvě karty nebo agentova žádost, která dorazí dvakrát, mohou všechny vyvolat víc publikací téže aplikace téměř ve stejnou chvíli.
V tomto případě přišly tři do 84 ms. Každá spustila celé nasazení. Na hostingové platformě se vydání pro tutéž aplikaci vytvořila ve stejné sekundě a tři z nich selhala, protože ostatní už běžela. Pozdější vydání uvízlo, ale aplikace pak fungovala.
Právě to to dělalo snadno přehlédnutelné. Uživatel viděl jednu fungující aplikaci. Problémy se projevily jen v e-mailech o selhaných vydáních.
Proč nic nezabránilo
Kód publikace měl krok, který se díval na běžící nasazení projektu. Ale dělal to tak, že všechna běžící nasazení před startem označil jako neúspěšná – „nahrazeno novou publikací“. Měl uklidit sestavení, která zůstala po pádu procesu.
V praxi to znamenalo, že druhá publikace, která dorazila uprostřed sestavení, prohlásila první za mrtvou a spustila vlastní. Obě doběhly až do konce proti téže aplikaci. Nebyl tu žádný zámek; byl tu krok, který aktivně uvolňoval cestu pro závod.
Objevil se i podivnější příznak u úplně první publikace aplikace. Protože ještě neexistoval záznam nasazení k opětovnému použití, dvě současné první publikace se pokusily vložit jeden. Databáze má jedinečný index na název odkazu aplikace (její slug, například tvoje-app v tvoje-app.mythex.ai), takže druhé vložení selhalo. API to přeložilo jako „tenhle název odkazu je obsazen“ a radilo uživateli přejmenovat aplikaci, která se v tu chvíli pod tímto názvem úspěšně publikovala.
Oprava: zámek je samotný nárok
Nárok na záznam nasazení je teď jediná podmíněná aktualizace databáze: vezmi tenhle řádek a označ ho jako sestavující jen pokud ho nikdo nepublikuje. Protože je to jeden příkaz, databáze vyřeší žádosti, které dorazí ve stejné milisekundě. Přesně jedna změní řádek. Ostatní zjistí, že se nezměnil žádný řádek, zastaví se a nikdy nedosáhnou orchestrátoru.
Zámek také potřebuje způsob, jak se uvolnit, když jeho držitel zemře. Pokud se API restartovalo uprostřed publikace, řádek by zůstal navždy označen jako „sestavuje se“ a aplikaci by nešlo publikovat. Proto se řádek, jehož vlastník přestal posílat srdeční tep – běžící publikace řádek aktualizuje každých 30 sekund a pět minut ticha se počítá jako mrtvo – může znovu nárokovat. Tento práh pochází z tepu, ne z odhadu, jak dlouho sestavení trvá. Sestavení vlastního Dockerfile může legitimně běžet dlouho, a pomalé sestavení, které stále tepe, si svůj nárok udrží.
Krok, který selhal všechna běžící nasazení, teď před publikací neběží. Uklidí se jen řádky, které tam zanechal mrtvý proces.
Říct poraženému pravdu
Žádost, která prohraje, dostane teď jasnou odpověď: publish_in_progress spolu s ID nasazení, které běží. Každé místo, které může publikaci spustit, to zvládne:
- Workspace sleduje sestavení, které už běží, místo aby nad ním zobrazil chybu.
- Agent dostane zprávu, že publikace této aplikace už běží, a má počkat, místo aby mu bylo řečeno, že něco selhalo – což ho dřív mohlo poslat „opravovat“ Dockerfile a zkoušet to znovu.
- Kolize při první publikaci na název odkazu se ověří: pokud řádek, který název drží, je běžící publikace tohoto vlastního projektu, odpověď zní „už se publikuje“, ne „ten název je obsazen“. Skutečná kolize s jiným projektem stále dostane zprávu o přejmenování.
Jak jsme to ověřili
Napsali jsme reprodukční skript, který spustí tři publikace jedné aplikace současně proti skutečné databázi a zástupnému orchestrátoru, a počítá, kolik nasazení orchestrátor dostane. Pokrývá první publikaci i opakovanou publikaci živé aplikace. Před opravou počítal dvě nasazení, po opravě jedno.
Skript také pokrývá únikovou cestu: označí řádek aplikace jako sestavující bez tepu po deset minut, znovu publikuje a ověří, že nová publikace řádek nárokuje a spustí se.
Jednotkové testy drží zámek u dvou věcí, které zámek musí dělat: ze tří současných opakovaných publikací projde přesně jedna a ostatní dvě dostanou zprávu, že publikace běží; a sestavení, jehož vlastník zemřel, lze nárokovat. Jiný test ověřuje, že nový pokus nepřenese chybovou zprávu předchozího pokusu.
Co jsme si z toho odnesli
- Úklidový kód může být skrytý závod. „Všechno běžící označ jako selhané, pak začni“ vypadá jako hygiena. Se dvěma volajícími je to každý, kdo ruší toho druhého.
- Nech rozhodnout databázi. Čtení následované zápisem nechává mezeru pro další žádost. Jediná podmíněná aktualizace ne.
- Každý zámek potřebuje expiraci svázanou s živostí. Bez ní se pád promění v zaseknutou aplikaci.
- Chybové zprávy jsou pokyny. „Ten název je obsazen“ lidi navedlo ke špatnému kroku. Správná zpráva byla jednodušší a pravdivá.
Více o publikaci v dokumentaci: jak publikovat. A související oprava: tvoje publikovaná aplikace zůstává online při opakované publikaci.