Dvojité stlačenie Publish teraz publikuje iba raz
Tri žiadosti o publikovanie tej istej aplikácie dorazili na naše API s odstupom 84 milisekúnd a každá spustila celý deploy tejto aplikácie. Navzájom sa predbiehali na rovnaký hosting, niekoľko výsledných releasov zlyhalo, pretože iný už dorazil prvý, a jediný viditeľný signál bol e-mail „release failed“ — samotná aplikácia nakoniec fungovala. Teraz publikovanie, ktoré prišlo, zatiaľ čo iné pre rovnakú aplikáciu beží, nespustí druhý build. Databáza rozhodne, ktorá žiadosť vyhrá, a ostatným sa povie pravda: publikovanie už prebieha.
Čo sa stalo
Keď publikujete aplikáciu v Mythex, API zapíše nasadenie a požiada náš orchestrátor — službu, ktorá spúšťa buildy — aby ju zostavil a vydal. Publikovanie sa môže spustiť tlačidlom Publish, z inej karty prehliadača alebo z agenta, keď o to požiadate. Dvojité kliknutie, dve karty alebo žiadosť agenta, ktorá dorazí dvakrát, môžu všetky vyvolať viac publikovaní tej istej aplikácie takmer v rovnakom okamihu.
V tomto prípade prišli tri v priebehu 84 ms. Každé spustilo celý deploy. Na hostingovej platforme sa releasy pre tú istú aplikáciu vytvorili v tej istej sekunde a tri z nich zlyhali, pretože ostatné už bežali. Neskorší release uviazol, ale aplikácia potom fungovala.
Práve preto to bolo ľahké prehliadnuť. Používateľ videl jednu funkčnú aplikáciu. Zlyhania sa ukázali len ako e-maily o zlyhaní releasu.
Prečo nič nezabránilo
Kód publikovania mal krok, ktorý pozeral na bežiace nasadenia projektu. Robil však to, že pred spustením označil každé bežiace nasadenie ako zlyhané — „nahradené novým publikovaním“. Mal upratať buildy zanechané po zlyhanom procese.
V praxi to znamenalo, že druhé publikovanie, ktoré prišlo uprostred buildu, prehlásilo prvé za mŕtve a spustilo vlastné. Obe bežali do konca proti tej istej aplikácii. Nebol tam žiadny zámok; bol tam krok, ktorý aktívne uvoľňoval cestu pre preteky.
Pri prvom publikovaní aplikácie bol aj druhý, podivnejší symptóm. Keďže ešte neexistoval záznam nasadenia na opätovné použitie, dve súbežné prvé publikovania sa obe pokúsili vložiť jeden. Databáza má unikátny index na názve odkazu aplikácie (jej slug, napríklad your-app v your-app.mythex.ai), takže druhé vloženie zlyhalo. API to preložilo na „tento názov odkazu je obsadený“ — a poradilo používateľovi premenovať aplikáciu, ktorá sa v tom momente pod týmto názvom úspešne publikovala.
Oprava: zámok je nárok
Nárok na záznam nasadenia je teraz jediná podmienená aktualizácia databázy: označ tento riadok ako building iba ak ho nikto nepublikuje. Pretože ide o jeden príkaz, databáza vyrieši žiadosti, ktoré dorazia v tej istej milisekunde. Presne jedna z nich zmení riadok. Ostatné vidia, že sa zmenilo nula riadkov, zastavia sa a orchestrátora nikdy nedosiahnu.
Zámok potrebuje aj cestu von, keď jeho držiteľ zomrie. Ak sa API reštartovalo uprostred publikovania, riadok by navždy zostal označený ako „building“ a aplikáciu by nebolo možné publikovať. Preto sa riadok, ktorého vlastník prestal posielať heartbeaty — bežiace publikovanie aktualizuje riadok každých 30 sekúnd a päť minút ticha sa počíta ako mŕtvy —, môže nárokovať znova. Tento prah pochádza z heartbeatu, nie z odhadu, ako dlho build trvá. Zostavenie vlastného Dockerfile používateľa môže legitímne trvať dlho a pomalý build, ktorý stále posiela heartbeaty, si svoj nárok zachová.
Krok, ktorý zlyhal všetky bežiace nasadenia, sa pred publikovaním už nespúšťa. Vyčistia sa len riadky, ktoré po sebe zanechal mŕtvy proces.
Povedať prehrávajúcemu pravdu
Žiadosť, ktorá prehrá, dostane jasnú odpoveď: publish_in_progress spolu s ID nasadenia, ktoré beží. Každé miesto, ktoré môže publikovanie spustiť, to rieši:
- Workspace nasleduje build, ktorý už beží, namiesto zobrazenia chyby nad ním.
- Agent je informovaný, že publikovanie tejto aplikácie už prebieha a má na neho počkať, namiesto toho, aby dostal správu o zlyhaní — čo ho predtým mohlo poslať „opravovať“ Dockerfile a skúšať znova.
- Kolízia pri prvom publikovaní na názve odkazu sa overí: ak riadok s týmto názvom patrí bežiacemu publikovaniu tohto projektu, odpoveď je „už sa publikuje“, nie „tento názov je obsadený“. Skutočná kolízia s iným projektom stále dostane správu o premenovaní.
Ako sme to overili
Napísali sme reprodukčný skript, ktorý spustí tri publikovania tej istej aplikácie naraz proti skutočnej databáze a falošnému orchestrátoru, a spočíta, koľko deployov orchestrátor dostal. Pokrýva prvé publikovanie aj opätovné publikovanie živej aplikácie. Pred opravou napočítal dva deploye, po oprave jeden.
Skript pokrýva aj únikovú cestu: označí riadok aplikácie ako building bez heartbeatu desať minút, znova publikuje a skontroluje, že nové publikovanie nárokuje riadok a spustí sa.
Unit testy držia zámok pri dvoch veciach, ktoré zámok musí robiť: z troch súbežných opätovných publikovaní prejde presne jedno a ďalšie dve sa dozvedia, že publikovanie prebieha; a build, ktorého vlastník zomrel, sa dá nárokovať. Ďalší test overuje, že nový pokus neprenesie chybovú správu predchádzajúceho pokusu.
Čo sme si z toho odniesli
- Upratovací kód môže byť skrytý preteky. „Zlyhaj všetko, čo beží, a potom začni“ znie ako hygiena. Pri dvoch volajúcich je to však každý, kto rusí toho druhého.
- Nechajte arbitrovať databázu. Čítanie nasledované zápisom nechá medzeru pre ďalšiu žiadosť. Jediná podmienená aktualizácia nie.
- Každý zámok potrebuje expiráciu viazanú na živosť. Bez nej sa pád zmení na zaseknutú aplikáciu.
- Chybové správy sú pokyny. „Ten názov je obsadený“ hovorilo ľuďom, aby urobili zlú vec. Správna správa bola jednoduchšia a pravdivá.
Viac o publikovaní je v dokumentácii: ako publikovať. A súvisiaca oprava: vaša publikovaná aplikácia zostáva online, keď ju znova publikujete.