Tryk på Publish to gange, og der publiceres stadig kun én gang
Tre publiceringsanmodninger for den samme app nåede vores API 84 millisekunder fra hinanden, og hver startede et fuldt deploy af appen. De kappedes om den samme hosting, flere af de resulterende releases fejlede, fordi en anden var kommet først, og det eneste synlige tegn var en e-mail om „release failed“ — appen selv endte fint. Nu starter en publicering, der kommer ind, mens en anden kører for den samme app, ikke et ekstra byg. Databasen afgør, hvilken anmodning der vinder, og de andre får at vide sandheden: en publicering kører allerede.
Hvad der skete
Når du publicerer en app på Mythex, registrerer API'en en deployment for den og beder vores orkestrator — den service, der kører builds — om at bygge og frigive den. Publicering kan starte fra knappen Publish, fra en anden browserfane eller fra agenten, når du beder den om at publicere. Et dobbeltklik, to faner eller en agentanmodning, der kommer to gange, kan alle give mere end én publicering for den samme app næsten samtidig.
I dette tilfælde kom tre inden for 84 ms. Hver kørte et fuldt deploy. På hostingplatformen blev releases for den samme app oprettet inden for det samme sekund, og tre af dem fejlede, fordi de andre allerede var i gang. En senere release gik i stå, og appen virkede bagefter.
Det var det, der gjorde det let at overse. Brugeren så én fungerende app. Fejlene viste sig kun som e-mails om fejlede releases.
Hvorfor intet stoppede det
Publiceringskoden havde faktisk et trin, der kiggede på deployments i gang for projektet. Men det, det gjorde, var at markere hver deployment i gang som fejlet før start — „erstattet af en ny publicering“. Det var tænkt til at rydde op i builds, der var efterladt af en proces, der var styrtet.
I praksis betød det, at en anden publicering, der kom ind midt i et byg, erklærede den første død og startede sin egen. Begge kørte til ende mod den samme app. Der var ingen lås; der var et trin, der aktivt ryddede vejen for et kapløb.
Der var et andet, mærkeligere symptom ved en apps allerførste publicering. Uden en deployment-post at genbruge endnu forsøgte to samtidige første publiceringer begge at indsætte én. Databasen har et unikt indeks på appens linknavn (dens slug, som your-app i your-app.mythex.ai), så den anden indsættelse fejlede. API'en oversatte det til „det linknavn er taget“ — og rådede brugeren til at omdøbe en app, der netop dér publicerede succesfuldt under det navn.
Rettelsen: claimet er låsen
At claime deployment-posten er nu en enkelt betinget databaseopdatering: tag denne række og markér den som bygning kun hvis ingen andre publicerer den. Fordi det er ét udsagn, afgør databasen anmodninger, der kommer i samme millisekund. Præcis én af dem ændrer rækken. De andre ser, at nul rækker blev ændret, stopper og når aldrig frem til orkestratoren.
En lås skal også have en udgang, når dens indehaver dør. Hvis API'en genstartede midt i en publicering, ville rækken stå som „building“ for evigt, og appen kunne ikke publiceres igen. Derfor kan en række, hvis ejer er holdt op med at sende heartbeats — den kørende publicering opdaterer rækken hvert 30. sekund, og fem minutters stilhed tæller som død — claimes igen. Den tærskel kommer fra heartbeatet, ikke fra et gæt på, hvor lang tid et byg tager. Et byg af brugerens egen Dockerfile kan legitimt køre længe, og et langsomt byg, der stadig sender heartbeats, beholder sit claim.
Trinnet, der markerede hver deployment i gang som fejlet, kører ikke længere før en publicering. Kun rækker, en død proces har efterladt, bliver ryddet op.
Fortæl taberen sandheden
Anmodningen, der taber, får nu et tydeligt svar: publish_in_progress sammen med ID'et for den deployment, der kører. Hvert sted, der kan starte en publicering, håndterer det:
- Workspacet følger det byg, der allerede kører, i stedet for at vise en fejl oven på det.
- Agenten får at vide, at en publicering af denne app allerede kører, og at den skal vente på den, i stedet for at få at vide, at noget fejlede — hvilket tidligere kunne få den til at gå i gang med at „rette“ en Dockerfile og prøve igen.
- En kollision ved første publicering på linknavnet tjekkes: hvis rækken, der holder navnet, er projektets egen kørende publicering, er svaret „publicerer allerede“, ikke „det navn er taget“. En reel kollision med et andet projekt får stadig omdøbningsbeskeden.
Sådan testede vi det
Vi skrev et reproduktionsscript, der kører tre publiceringer af én app samtidig mod en rigtig database og en stub-orkestrator, og tæller, hvor mange deploys orkestratoren bliver bedt om. Det dækker en første publicering og en genudgivelse af en levende app. Før rettelsen talte det to deploys. Efter én.
Scriptet dækker også nødudgangen: det markerer appens række som bygning uden heartbeat i ti minutter, publicerer igen og kontrollerer, at den nye publicering claimer rækken og kører.
Enhedstests holder låsen til de to ting, en lås skal gøre: af tre samtidige genudgivelser slipper præcis én igennem, og de to andre får at vide, at en publicering kører; og et byg, hvis ejer døde, kan claimes. En anden test kontrollerer, at et nyt forsøg ikke bærer den forrige forsøgs fejlbesked med sig.
Hvad vi tog med
- Oprydningskode kan være et kapløb i forklædning. „Fejl alt, der kører, og start så“ ligner hygiejne. Med to kaldere er det hver af dem, der annullerer den anden.
- Lad databasen afgøre. Et læs efterfulgt af et skriv efterlader et hul for en anden anmodning. Et enkelt betinget update gør ikke.
- Enhver lås har brug for en udløbstid knyttet til liveness. Uden den bliver et nedbrud til en fast app.
- Fejlbeskeder er instruktioner. „Det navn er taget“ fortalte folk at gøre det forkerte. Den rigtige besked var enklere og sand.
Mere om publicering i dokumentationen: sådan publicerer du. Og en relateret rettelse: din publicerede app forbliver oppe, mens du republicerer den.