Å trykke Publish to ganger publiserer nå bare én gang
Tre publiseringsforespørsler for samme app nådde API-et vårt 84 millisekunder fra hverandre, og hver av dem startet en full deploy av appen. De kappløp om den samme hostingen, flere av de resulterende utgivelsene feilet fordi en annen hadde kommet først, og det eneste synlige tegnet var en «release failed»-e-post — appen i seg selv endte opp fin. Nå starter ikke en publisering som kommer mens en annen kjører for samme app, et nytt bygg. Databasen avgjør hvilken forespørsel som vinner, og de andre får beskjed om sannheten: en publisering kjører allerede.
Hva som skjedde
Når du publiserer en app på Mythex, registrerer API-et en deployment for den og ber orkestratoren vår — tjenesten som kjører byggene — om å bygge og utgi den. Publisering kan starte fra Publish-knappen, fra en annen nettleserfane, eller fra agenten når du ber den publisere. Et dobbeltklikk, to faner eller en agentforespørsel som kommer to ganger kan alle gi mer enn én publisering for samme app nesten samtidig.
I dette tilfellet kom tre innen 84 ms. Hver kjørte en full deploy. På hostingplattformen ble utgivelser for samme app opprettet i samme sekund, og tre av dem feilet fordi de andre allerede var i gang. En senere utgivelse satt fast, og appen fungerte etterpå.
Det var det som gjorde det lett å overse. Brukeren så én fungerende app. Feilene dukket bare opp som e-poster om utgivelsesfeil.
Hvorfor ingenting stoppet det
Publiseringskoden hadde faktisk et steg som så på deployer som pågikk for prosjektet. Men det det gjorde, var å markere hver pågående deploy som feilet før start — «superseded by a new publish». Det var ment til å rydde opp i bygg som ble liggende etter en prosess som krasjet.
I praksis betydde det at en andre publisering som kom midt i et bygg, erklærte den første død og startet sin egen. Begge kjørte til ende mot samme app. Det fantes ingen lås; det fantes et steg som aktivt ryddet vei for et kappløp.
Det var også et merkeligere symptom ved en apps aller første publisering. Uten en deploymentpost å gjenbruke ennå, prøvde to samtidige første publiseringer begge å sette inn én. Databasen har et unikt indeks på appens lenkenavn (slugen, som your-app i your-app.mythex.ai), så den andre innsettingen feilet. API-et oversatte det til «det lenkenavnet er tatt» — og ga brukeren råd om å gi nytt navn til en app som, på det tidspunktet, ble publisert uten problem under det navnet.
Løsningen: kravet er låsen
Å kreve deploymentposten er nå én betinget databaseoppdatering: ta denne raden og merk den som under bygging bare hvis ingen publiserer den. Fordi det er én setning, avgjør databasen forespørsler som kommer i samme millisekund. Nøyaktig én av dem endrer raden. De andre ser at null rader ble endret, stopper, og når aldri orkestratoren.
En lås trenger også en utvei når innehaveren dør. Hvis API-et startet på nytt midt i en publisering, ville raden stå merket «building» for alltid, og appen kunne ikke publiseres igjen. Så en rad hvis eier har sluttet å sende heartbeats — den kjørende publiseringen oppdaterer raden hvert 30. sekund, og fem minutter uten signal teller som død — kan kreves på nytt. Den terskelen kommer fra heartbeaten, ikke fra en gjetning på hvor lang tid et bygg tar. Å bygge brukerens egen Dockerfile kan legitimt ta lang tid, og et tregt bygg som fortsatt sender heartbeat, beholder sitt krav.
Steget som feilet alle pågående deployer, kjører ikke lenger før en publisering. Bare rader som en død prosess etterlot seg, ryddes.
Å fortelle taperen sannheten
Forespørselen som taper, får nå et tydelig svar: publish_in_progress, sammen med ID-en til deploymenten som kjører. Hvert sted som kan starte en publisering, håndterer det:
- Arbeidsområdet følger bygget som allerede kjører, i stedet for å vise en feil over det.
- Agenten får beskjed om at en publisering av denne appen allerede kjører, og å vente på den, i stedet for å bli fortalt at noe feilet — noe som tidligere kunne sende den av sted for å «fikse» en Dockerfile og prøve igjen.
- En kollisjon ved første publisering på lenkenavnet sjekkes: hvis raden som holder navnet er prosjektets egen kjørende publisering, er svaret «already publishing», ikke «that name is taken». En reell kollisjon med et annet prosjekt får fortsatt meldingen om å gi nytt navn.
Slik sjekket vi det
Vi skrev et reproduksjonsskript som kjører tre publiseringer av én app samtidig mot en ekte database og en stubbet orkestrator, og teller hvor mange deploys orkestratoren blir bedt om. Det dekker en første publisering og en ny publisering av en live app. Før løsningen talte det to deploys. Etter, én.
Skriptet dekker også utveien: det markerer appens rad som under bygging uten heartbeat i ti minutter, publiserer på nytt, og sjekker at den nye publiseringen tar over raden og kjører.
Enhetstester holder låsen til de to tingene en lås må gjøre: av tre samtidige nypublikasjoner slipper nøyaktig én gjennom, og de to andre får beskjed om at en publisering kjører; og et bygg hvis eier døde, kan tas over. En annen test sjekker at et nytt forsøk ikke bærer med seg forrige forsøks feilmelding.
Det vi tok med oss
- Opprydningskode kan være et kappløp i forkledning. «Feil alt som kjører, start så» ser ut som hygiene. Med to kallere er det at hver avbryter den andre.
- La databasen avgjøre. En lesing etterfulgt av en skriving lar et gap stå åpent for en annen forespørsel. En enkelt betinget oppdatering gjør ikke det.
- Hver lås trenger en utløpstid knyttet til liveness. Uten den blir et krasj til en fastlåst app.
- Feilmeldinger er instruksjoner. «That name is taken» ba folk gjøre feil ting. Den riktige meldingen var enklere og sann.
Mer om publisering i dokumentasjonen: slik publiserer du. Og en beslektet rettelse: publisert app forblir oppe mens du republiserer den.