Pritisak na Objavi dvaput sada objavljuje jednom
Tri zahtjeva za objavu iste aplikacije stigla su do našeg API-ja s razmakom od 84 milisekunde, i svaki je pokrenuo punu izgradnju te aplikacije. Trkali su se da stignu na isti hosting, nekoliko nastalih izdanja nije uspjelo jer je drugo već stiglo prvo, a jedini vidljivi znak bio je e-mail „izdanje nije uspjelo” — sama aplikacija je na kraju bila u redu. Sada objava koja stigne dok se druga za istu aplikaciju već izvršava ne pokreće drugu izgradnju. Baza odlučuje koji zahtjev pobjeđuje, a ostali dobijaju istinu: objava je već u toku.
Šta se desilo
Kada objavite aplikaciju na Mythexu, API zabilježi deployment za nju i traži od našeg orkestratora — servisa koji pokreće izgradnje — da je izgradi i izda. Objava može početi s dugmeta Objavi, iz drugog taba pregledača ili od agenta kada ga zamolite da objavi. Dvostruki klik, dva taba ili zahtjev agenta koji stigne dvaput mogu svi proizvesti više objava iste aplikacije u gotovo isto vrijeme.
U ovom slučaju, tri su stigla u roku od 84 ms. Svaka je pokrenula punu izgradnju. Na hosting platformi izdanja za istu aplikaciju kreirana su u istoj sekundi, a tri su nisu uspjela jer su ostala već bila u toku. Jedno kasnije izdanje je zapelo, a aplikacija je nakon toga radila.
Upravo to je otežavalo da se primijeti. Korisnik je vidio jednu radnu aplikaciju. Greške su se pojavljivale samo kao e-mailovi o neuspjelom izdanju.
Zašto ništa nije zaustavilo
Kod objave je zaista imao korak koji je gledao deployment-e u toku za projekat. Ali ono što je radio bilo je da označi svaki deployment u toku kao neuspio prije početka — „zamijenjen novom objavom”. Trebalo je očistiti izgradnje koje je ostavio srušeni proces.
U praksi, to je značilo da druga objava koja stigne usred izgradnje proglasi prvu mrtvom i pokrene svoju. Obje su se izvršile do kraja nad istom aplikacijom. Nije postojao zaključavanje; postojao je korak koji je aktivno čistio put za trku.
Postojao je i drugi, čudniji simptom za prvu objavu aplikacije. Pošto još nije bilo zapisa o deploymentu koji bi se ponovo koristio, dvije istovremene prve objave pokušale su obje da ubace jedan. Baza ima jedinstveni indeks na nazivu linka aplikacije (njen slug, kao your-app u your-app.mythex.ai), pa je drugi unos pao. API je to preveo u „taj naziv linka je zauzet” — savjetujući korisniku da preimenuje aplikaciju koja se u tom trenutku uspješno objavljivala pod tim imenom.
Rješenje: zahtjev za zapis je zaključavanje
Zauzimanje zapisa o deploymentu sada je jedno uslovno ažuriranje u bazi: uzmi ovaj red i označi ga kao u izgradnji samo ako ga niko ne objavljuje. Pošto je to jedna naredba, baza rješava zahtjeve koji stignu u istoj milisekundi. Tačno jedan ga mijenja. Ostali vide da se nijedan red nije promijenio, zaustave se i nikada ne dođu do orkestratora.
Zaključavanje također treba izlaz kada njegov vlasnik umre. Ako se API restartovao usred objave, red bi ostao označen kao „u izgradnji” zauvijek, a aplikacija se ne bi mogla objaviti. Zato red čiji vlasnik prestane slati heartbeat — aktivna objava ažurira red svakih 30 sekundi, a pet minuta tišine znači da je mrtav — može biti ponovo zauzet. Taj prag dolazi iz heartbeata, a ne iz nagađanja koliko izgradnja traje. Izgradnja korisnikovog vlastitog Dockerfile-a može legitimno trajati dugo, a spora izgradnja koja i dalje šalje heartbeat zadržava svoje zauzeće.
Korak koji je označavao svaki deployment u toku kao neuspio više se ne izvršava prije objave. Čiste se samo redovi koje je ostavio mrtav proces.
Reći gubitniku istinu
Zahtjev koji gubi sada dobija jasan odgovor: publish_in_progress, uz ID deploymenta koji se izvršava. Svako mjesto koje može pokrenuti objavu to obrađuje:
- Radni prostor prati izgradnju koja je već u toku umjesto da preko nje prikaže grešku.
- Agent dobija poruku da objava ove aplikacije već radi i da na nju čeka, umjesto da mu se kaže da je nešto propalo — što ga je ranije moglo odvesti da „popravlja” Dockerfile i pokuša ponovo.
- Sudar pri prvoj objavi na nazivu linka se provjerava: ako je red koji drži naziv aktivna objava vlastitog projekta, odgovor je „već se objavljuje”, a ne „taj naziv je zauzet”. Pravi sudar s drugim projektom i dalje dobija poruku o preimenovanju.
Kako smo provjerili
Napisali smo skriptu za reprodukciju koja pokreće tri objave jedne aplikacije u isto vrijeme protiv prave baze i lažnog orkestratora, i broji koliko je deployova orkestrator dobio zahtjev za izvršenje. Pokriva prvu objavu i ponovnu objavu žive aplikacije. Prije popravke brojala je dva deploya. Poslije, jedan.
Skripta također pokriva izlaz u nuždi: označava red aplikacije kao „u izgradnji” bez heartbeata deset minuta, ponovo objavljuje i provjerava da nova objava zauzme red i izvrši se.
Jedinični testovi drže zaključavanje na dvije stvari koje zaključavanje mora raditi: od tri istovremene ponovne objave tačno jedna prolazi, a ostale dvije dobijaju poruku da objava radi; i izgradnja čiji je vlasnik umro može se zauzeti. Drugi test provjerava da novi pokušaj ne nasljeđuje poruku o grešci prethodnog pokušaja.
Šta smo izvukli iz ovoga
- Kod za čišćenje može biti trka u prerušenom obliku. „Označi sve što je u toku kao neuspjelo, pa počni” liči na higijenu. Sa dva pozivaoca, svaki poništava drugog.
- Neka baza bude arbitar. Čitanje pa pisanje ostavlja prazninu za drugi zahtjev. Jedno uslovno ažuriranje to ne ostavlja.
- Svako zaključavanje treba isteći vezano za živost. Bez toga, pad pretvara aplikaciju u zaglavljenu.
- Poruke o greškama su uputstva. „Taj naziv je zauzet” navodilo je ljude da urade pogrešnu stvar. Ispravna poruka bila je jednostavnija i tačna.
Više o objavi u dokumentaciji: kako objaviti. I srodna popravka: vaša objavljena aplikacija ostaje aktivna dok je ponovo objavljujete.