Apăsatul de două ori pe Publish publică acum o singură dată
Trei cereri de publicare pentru aceeași aplicație au ajuns la API-ul nostru la 84 de milisecunde distanță, iar fiecare a pornit un deploy complet al aplicației. Se luptau între ele pentru același hosting, iar mai multe dintre release-urile rezultate au eșuat pentru că altul ajunsese deja primul; singurul semn vizibil era un email „release failed” — aplicația în sine a ajuns bine până la urmă. Acum, o publicare care sosește cât timp alta rulează deja pentru aceeași aplicație nu mai pornește un al doilea build. Baza de date decide care cerere câștigă, iar celelalte primesc adevărul: o publicare rulează deja.
Ce s-a întâmplat
Când publici o aplicație pe Mythex, API-ul înregistrează un deployment pentru ea și cere orchestratorului nostru — serviciul care rulează build-urile — să o construiască și să o lanseze. Publicarea poate porni din butonul Publish, dintr-un alt tab al browserului sau din agent, când îi ceri să publice. Un dublu clic, două tab-uri sau o cerere de agent care sosește de două ori pot produce mai multe publicări pentru aceeași aplicație aproape în același moment.
În cazul de față, trei au sosit în 84 ms. Fiecare a rulat un deploy complet. Pe platforma de hosting, release-urile pentru aceeași aplicație au fost create în aceeași secundă, iar trei dintre ele au eșuat pentru că celelalte erau deja în desfășurare. Un release ulterior s-a blocat, dar aplicația a funcționat după aceea.
Asta a făcut-o ușor de ratat. Utilizatorul a văzut o singură aplicație care funcționa. Eșecurile apăreau doar ca emailuri de tip release-failure.
De ce nimic nu a oprit-o
Codul de publicare avea într-adevăr un pas care se uita la deploy-urile în curs pentru proiect. Dar ce făcea era să marcheze ca eșuate, înainte de pornire, toate deploy-urile în curs — „înlocuit de o publicare nouă”. Era gândit să curețe build-urile rămase în urma unui proces căzut.
În practică, asta însemna că o a doua publicare care sosea în mijlocul unui build declara primul ca mort și pornea propriul build. Ambele rulau până la capăt împotriva aceleiași aplicații. Nu exista niciun lacăt; exista un pas care activ elibera drumul pentru o cursă.
Exista și un simptom mai ciudat, pentru prima publicare a unei aplicații. Fără o înregistrare de deployment de refolosit, două prime publicări concurente încercau amândouă să inserteze una. Baza de date are un index unic pe numele de link al aplicației (slug-ul ei, precum your-app din your-app.mythex.ai), deci a doua inserare eșua. API-ul traducea asta în „numele acela de link este deja folosit” — sfătuind utilizatorul să redenumească o aplicație care, chiar în acel moment, se publica cu succes sub acel nume.
Soluția: revendicarea este lacătul
Revendicarea înregistrării de deployment este acum o singură actualizare condiționată în baza de date: ia acest rând și marchează-l ca în construire doar dacă nimeni nu-l publică. Pentru că este o singură instrucțiune, baza de date rezolvă cererile care sosesc în aceeași milisecundă. Exact una dintre ele schimbă rândul. Celelalte văd că zero rânduri au fost schimbate, se opresc și nu ajung niciodată la orchestrator.
Un lacăt are nevoie și de o ieșire când deținătorul lui moare. Dacă API-ul repornea în mijlocul unei publicări, rândul ar rămâne marcat „building” pentru totdeauna, iar aplicația nu s-ar mai putea publica. Așa că un rând al cărui proprietar a încetat să trimită heartbeat-uri — publicarea în curs actualizează rândul la fiecare 30 de secunde, iar cinci minute de tăcere înseamnă mort — poate fi revendicat din nou. Pragul vine din heartbeat, nu dintr-o presupunere despre cât durează un build. Construirea unui Dockerfile propriu al utilizatorului poate dura legitim mult, iar un build lent care încă trimite heartbeat își păstrează revendicarea.
Pasul care marca drept eșuate toate deploy-urile în curs nu mai rulează înainte de o publicare. Sunt curățate doar rândurile rămase în urma unui proces mort.
Să îi spunem celui care pierde adevărul
Cererea care pierde primește acum un răspuns clar: publish_in_progress, împreună cu ID-ul deployment-ului care rulează. Fiecare loc care poate porni o publicare tratează asta:
- Spațiul de lucru urmărește build-ul care rulează deja, în loc să afișeze o eroare peste el.
- Agentul primește vestea că o publicare a acestei aplicații rulează deja și trebuie să aștepte, în loc să fie anunțat că ceva a eșuat — ceea ce putea să-l trimită înainte să „repare” un Dockerfile și să încerce din nou.
- O coliziune la prima publicare pe numele de link este verificată: dacă rândul care deține numele este propria publicare în curs a acestui proiect, răspunsul este „se publică deja”, nu „numele este deja folosit”. O coliziune reală cu un alt proiect primește în continuare mesajul de redenumire.
Cum am verificat
Am scris un script de reproducere care rulează trei publicări ale aceleiași aplicații în același timp, pe o bază de date reală și cu un orchestrator de test, și numără câte deploy-uri sunt cerute orchestratorului. Acoperă o primă publicare și o republicare a unei aplicații live. Înainte de soluție, numără două deploy-uri. După, unul.
Scriptul acoperă și ieșirea de siguranță: marchează rândul aplicației ca în construire fără heartbeat timp de zece minute, publică din nou și verifică dacă noua publicare revendică rândul și rulează.
Testele unitare țin lacătul la cele două lucruri pe care trebuie să le facă un lacăt: din trei republicări simultane, exact una trece, iar celelalte două sunt anunțate că o publicare rulează; și un build al cărui proprietar a murit poate fi revendicat. Un alt test verifică faptul că o nouă încercare nu moștenește mesajul de eroare al încercării anterioare.
Ce am învățat
- Codul de curățare poate fi o cursă deghizată. „Eșuează tot ce e în curs, apoi pornește” pare igienă. Cu doi apelanți, fiecare îl anulează pe celălalt.
- Lasă baza de date să decidă. O citire urmată de o scriere lasă loc unei alte cereri. O singură actualizare condiționată nu.
- Fiecare lacăt are nevoie de expirare legată de viață. Fără ea, o prăbușire se transformă într-o aplicație blocată.
- Mesajele de eroare sunt instrucțiuni. „Numele acela este deja folosit” îi spunea oamenilor să facă lucrul greșit. Mesajul corect era mai simplu și adevărat.
Mai multe despre publicare în documentație: cum publici. Și o reparație înrudită: aplicația ta publicată rămâne online cât timp o republici.