Dvakratni pritisk na Publish zdaj objavi samo enkrat
Trije zahtevki za objavo iste aplikacije so prispeli do našega API-ja 84 milisekund narazen, vsak pa je sprožil celotno izdajo te aplikacije. Tekmovali so med seboj za isto gostovanje, več nastalih izdaj je spodletelo, ker je kateri drug že prišel prej, edini vidni znak pa je bil e-poštni sporočilo „release failed“ (izdaja neuspešna) – sama aplikacija je na koncu delovala. Zdaj objava, ki pride, medtem ko za isto aplikacijo že teče druga, ne zažene novega gradnika. Baza odloči, kateri zahtevek zmaga, ostali pa dobijo resnico: objava je že v teku.
Kaj se je zgodilo
Ko objavite aplikacijo v Mythexu, API zabeleži izdajo za to aplikacijo in prosi naš orkestrator – storitev, ki izvaja gradnje – naj jo zgradi in izda. Objava se lahko začne z gumba Publish, iz drugega zavihka brskalnika ali od agenta, ko ga prosite za objavo. Dvojni klik, dva zavihka ali zahteva agenta, ki pride dvakrat, lahko vse povzročijo več objav iste aplikacije skoraj hkrati.
V tem primeru so trije prispeli v 84 ms. Vsak je sprožil celotno izdajo. Na platformi za gostovanje so se izdaje za isto aplikacijo ustvarile v isti sekundi, tri od njih pa so spodletele, ker so bile druge že v teku. Ena kasnejša izdaja se je zataknila, a je aplikacija kasneje delovala.
Zato je bilo težko opaziti. Uporabnik je videl eno delujočo aplikacijo. Napake so se pokazale samo v e-poštnih sporočilih o neuspešni izdaji.
Zakaj ni nič ustavilo
Koda za objavo je imela korak, ki je pogledal izdaje v teku za projekt. Vendar je vsako izdajo v teku pred začetkom označil kot neuspešno – „superseded by a new publish“ (nadomeščeno z novo objavo). Namenjen je bil čiščenju gradenj, ki so ostale za seboj po zrušenem procesu.
V praksi je to pomenilo, da je druga objava, ki je prišla med gradnjo prve, prvo razglasila za mrtvo in zagnala svojo. Obe sta tekli do konca nad isto aplikacijo. Zaklepanja ni bilo; obstajal je korak, ki je aktivno počistil pot za tekmo.
Pri prvi objavi aplikacije je bil pojav še bolj čuden. Ker še ni bilo zapisa izdaje za ponovno uporabo, sta dve sočasni prvi objavi poskusili vstaviti enega. Baza ima enoličen indeks na povezavi imena aplikacije (njenega slug-a, na primer your-app v your-app.mythex.ai), zato je drugi vstavek spodletel. API je to prevedel kot „that link name is taken“ (to ime povezave je zasedeno) – uporabnika je pozval, naj preimenuje aplikacijo, ki se je v tistem trenutku uspešno objavljala pod tem imenom.
Popravek: zaklep je zahtevek
Zdaj je prevzem zapisa izdaje en pogojni posodobitev v bazi: vzemi to vrstico in jo označi kot v gradnji samo, če je ni nihče objavljal. Ker je to en sam stavek, baza uredi zahtevke, ki prispejo v isti milisekundi. Natanko eden spremeni vrstico. Ostali vidijo, da se ni spremenila nobena vrstica, se ustavijo in nikoli ne pridejo do orkestratorja.
Zaklep potrebuje tudi izhod, kadar njegov imetnik umre. Če se je API med objavo znova zagnal, bi vrstica ostala označena kot „building“ za vedno in aplikacije ne bi bilo mogoče objaviti. Zato je vrstico, katere lastnik preneha pošiljati srčne utripe – tekoča objava posodobi vrstico vsakih 30 sekund, pet minut tišine pa šteje kot mrtvo –, mogoče prevzeti znova. Ta prag prihaja iz srčnega utripa, ne iz ugibanja, koliko časa traja gradnja. Gradnja uporabnikovega lastnega Dockerfile-a lahko upravičeno traja dolgo, gradnja, ki še vedno pošilja srčne utripe, pa obdrži svoj zaklep.
Korak, ki je označil vse izdaje v teku kot neuspešne, se pred objavo več ne izvaja. Počistijo se le vrstice, ki jih je pustil mrtav proces.
Ostalim povedati resnico
Zahtevek, ki izgubi, zdaj dobi jasen odgovor: publish_in_progress, skupaj z ID-jem izdaje, ki teče. Vsako mesto, ki lahko sproži objavo, to obravnava:
- Delovni prostor sledi gradnji, ki že teče, namesto da bi čez njo prikazal napako.
- Agent dobi sporočilo, da objava te aplikacije že teče, in naj počaka nanjo, namesto da bi mu bilo sporočeno, da je nekaj spodletelo – kar ga je prej lahko poslalo, da „popravi“ Dockerfile in poskusi znova.
- Trk pri prvi objavi na imenu povezave se preveri: če vrstica, ki drži ime, pripada tekoči objavi tega istega projekta, je odgovor „already publishing“ (že se objavlja), ne „that name is taken“ (ime je zasedeno). Pravi trk z drugim projektom še vedno dobi sporočilo o preimenovanju.
Kako smo preverili
Napisali smo skript za reprodukcijo, ki sočasno zažene tri objave ene aplikacije na pravi bazi s posnemanim orkestratorjem in šteje, koliko izdaj orkestrator dobi. Zajema prvo objavo in ponovno objavo žive aplikacije. Pred popravkom je štel dve izdaji. Po njem ena.
Skript zajema tudi izhod v sili: označi vrstico aplikacije kot „building“ brez srčnega utripa deset minut, ponovno objavi in preveri, da nova objava prevzame vrstico in se izvede.
Enotski testi držijo zaklep pri dveh stvareh, ki jih mora zaklep narediti: od treh sočasnih ponovnih objav natanko ena pride skozi, ostali dve pa dobita sporočilo, da objava teče; in gradnjo, katere lastnik je umrl, je mogoče prevzeti. Drug test preveri, da novi poskus ne prenese napake prejšnjega poskusa.
Kaj smo si iz tega vzeli
- Čiščenje je lahko skrit spopad. „Prekini vse, kar teče, nato začni“ zveni kot higiena. Pri dveh klicateljih pa gre za to, da se drug drugega prekinjata.
- Naj baza odloča. Branje, ki mu sledi pisanje, pušča vrzel za drug zahtevek. En sam pogojni posodobitev je ne pušča.
- Vsak zaklep potrebuje rok veljavnosti, vezan na živost. Brez tega se zruši spremeni v zataknjeno aplikacijo.
- Sporočila o napakah so navodila. „That name is taken“ je ljudi napotilo k napačnemu dejanju. Pravo sporočilo je bilo preprostejše in resnično.
Več o objavljanju v dokumentaciji: kako objaviti. Sorodni popravek: vaša objavljena aplikacija ostane delujoča, medtem ko jo ponovno objavljate.