Pritisak na Publish dvaput sada objavljuje jednom
Tri zahtjeva za objavu iste aplikacije stigla su na naš API s razmakom od 84 milisekunde, i svaki je pokrenuo puni deploy te aplikacije. Utrkivali su se da stignu na isti hosting, a nekoliko nastalih izdanja nije uspjelo jer je neki drugi već stigao prije njih. Jedini vidljiv znak bila je e-poruka „release failed”, a sama aplikacija je na kraju bila u redu. Sada objava koja stigne dok je druga za istu aplikaciju u tijeku ne pokreće drugi build. Baza odlučuje koji zahtjev pobjeđuje, a ostali dobivaju istinu: objava je već u tijeku.
Što se dogodilo
Kad objavite aplikaciju na Mythexu, API bilježi deployment za nju i traži od našeg orkestratora, servisa koji vodi buildove, da je izgradi i pusti. Objava može krenuti s gumba Publish, iz druge kartice preglednika ili od agenta kad to zatražite. Dvostruki klik, dvije kartice ili agentski zahtjev koji stigne dvaput mogu sve proizvesti više objava za istu aplikaciju u gotovo isto vrijeme.
U ovom slučaju stigla su tri unutar 84 ms. Svaki je pokrenuo puni deploy. Na hosting platformi izdanja za istu aplikaciju kreirana su u istoj sekundi, a tri su pala jer su druga već bila u tijeku. Kasnije izdanje je zapelo, ali aplikacija je nakon toga radila.
Upravo je to olakšavalo promašaj. Korisnik je vidio jednu radnu aplikaciju. Neuspjesi su se pojavili samo kao e-poruke o neuspjelim izdanjima.
Zašto ništa nije zaustavilo
Kod objave je postojao korak koji je gledao deploye u tijeku za projekt. Ali ono što je radio bilo je da prije pokretanja označi svaki deploy u tijeku kao neuspješan, s porukom „superseded by a new publish”. Trebao je počistiti buildove koje je ostavio pao proces.
U praksi je to značilo da druga objava koja stigne usred builda proglasi prvu mrtvom i pokrene vlastitu. Obje su se izvršile do kraja protiv iste aplikacije. Nije bilo zaključavanja; postojao je korak koji je aktivno čistio put za utrku.
Za prvu objavu aplikacije postojao je i drugi, čudniji simptom. Bez zapisa deploymenta koji bi se mogao ponovno koristiti, dvije istovremene prve objave pokušale su umetnuti isti zapis. Baza ima jedinstveni indeks na poveznici aplikacije (njen slug, npr. your-app u your-app.mythex.ai), pa je drugi umetak pao. API je to preveo u „taj naziv poveznice je zauzet”, savjetujući korisniku da preimenuje aplikaciju koja se u tom trenutku uspješno objavljivala pod tim imenom.
Popravak: zahtjev je zaključavanje
Zauzimanje zapisa deploymenta sada je jedno uvjetno ažuriranje u bazi: uzmi ovaj red i označi ga kao building samo ako ga nitko ne objavljuje. Zbog jedne naredbe baza rješava zahtjeve koji stignu u istoj milisekundi. Točno jedan ga mijenja. Ostali vide da nijedan red nije promijenjen, zaustave se i nikad ne stignu do orkestratora.
Zaključavanju treba i izlaz kad mu vlasnik umre. Ako se API ponovno pokrenuo usred objave, red bi ostao označen kao „building” zauvijek i aplikacija se ne bi mogla objaviti. Zato se red čiji vlasnik prestaje slati heartbeat može ponovno zauzeti: pokrenuta objava ažurira red svakih 30 sekundi, a pet minuta tišine znači mrtav. Taj prag dolazi iz heartbeata, a ne iz nagađanja koliko build traje. Građenje korisničkog Dockerfilea može legitimno trajati dugo, a spori build koji i dalje šalje heartbeat zadržava svoje zauzeće.
Korak koji je označavao sve deploye u tijeku kao neuspješne više se ne pokreće prije objave. Čiste se samo redovi koje je ostavio mrtav proces.
Reći gubitniku istinu
Zahtjev koji izgubi sada dobiva jasan odgovor: publish_in_progress, uz ID deploymenta koji je u tijeku. Svako mjesto koje može pokrenuti objavu to obrađuje:
- Radni prostor prati build koji je već u tijeku umjesto da prikaže grešku preko njega.
- Agent dobiva poruku da je objava ove aplikacije već u tijeku i da treba pričekati, umjesto da mu se kaže da je nešto palo, što ga je prije moglo natjerati da „popravlja” Dockerfile i pokušava ponovno.
- Sudar pri prvoj objavi na nazivu poveznice se provjerava: ako je zapis koji drži naziv ovog projekta vlastita objava u tijeku, odgovor je „već se objavljuje”, a ne „naziv je zauzet”. Stvarni sudar s drugim projektom i dalje dobiva poruku o preimenovanju.
Kako smo provjerili
Napisali smo skriptu za reprodukciju koja pokreće tri objave jedne aplikacije u isto vrijeme, na pravoj bazi i s lažnim orkestratorom, i broji koliko deployeja orkestrator dobije. Pokriva prvu objavu i ponovnu objavu žive aplikacije. Prije popravka brojala je dva deploya, nakon popravka jedan.
Skripta pokriva i izlaz za nuždu: označi red aplikacije kao building bez heartbeata deset minuta, ponovno objavi i provjeri da nova objava zauzima red i pokreće se.
Jedinični testovi drže zaključavanje na dvije stvari koje zaključavanje mora raditi: od tri istovremene ponovne objave točno jedna prolazi, a ostale dvije dobivaju poruku da objava traje; i build č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.
Što smo iz toga izvukli
- Kod za čišćenje može biti utrka u maskiranju. „Označi sve što je u tijeku kao neuspjelo, pa kreni” zvuči kao higijena. Uz dva pozivatelja, svaki poništava drugoga.
- Pustite bazu da odlučuje. Čitanje pa pisanje ostavlja prostor za drugi zahtjev. Jedno uvjetno ažuriranje ne ostavlja.
- Svako zaključavanje treba istek vezan uz živost. Bez toga se pad pretvara u zaglavljenu aplikaciju.
- Poruke o greškama su upute. „Taj naziv je zauzet” govorilo je ljudima da učine krivu stvar. Ispravna poruka bila je jednostavnija i istinita.
Više o objavi u dokumentaciji: kako objaviti. I povezana promjena: objavljena aplikacija ostaje dostupna dok je ponovno objavljujete.