Twee keer op Publish drukken publiceert nu één keer
Drie publicatieverzoeken voor dezelfde app bereikten onze API 84 milliseconden uit elkaar, en elk startte een volledige deploy van die app. Ze liepen elkaar tegen het lijf op dezelfde hosting, en verschillende van de resulterende releases mislukten omdat een ander eerder was geweest. Het enige zichtbare teken was een e-mail “release failed”; de app zelf bleef uiteindelijk goed. Nu start een publicatie die binnenkomt terwijl er voor dezelfde app al een loopt, geen tweede build. De database beslist welk verzoek wint, en de anderen krijgen het juiste bericht: er wordt al gepubliceerd.
Wat er gebeurde
Wanneer je een app op Mythex publiceert, legt de API een deployment vast voor die app en vraagt onze orchestrator, de service die builds draait, om die te bouwen en vrij te geven. Publiceren kan starten vanaf de knop Publish, vanuit een ander browsertabblad, of vanuit de agent wanneer je erom vraagt. Een dubbele klik, twee tabbladen, of een agentverzoek dat twee keer binnenkomt, kunnen allemaal meer dan één publicatie voor dezelfde app veroorzaken bijna op hetzelfde moment.
In dit geval kwamen er drie binnen 84 ms. Elk draaide een volledige deploy. Op het hostingplatform werden releases voor dezelfde app in dezelfde seconde aangemaakt, en drie ervan mislukten omdat de andere al liepen. Een latere release bleef hangen, en de app werkte daarna.
Dat maakte het makkelijk om te missen. De gebruiker zag één werkende app. De fouten kwamen alleen naar boven als e-mails over mislukte releases.
Waarom niets het tegenhield
De publicatiecode had wel een stap die naar lopende deploys van het project keek. Maar wat die deed, was elke lopende deploy als mislukt markeren vóór het starten: “vervangen door een nieuwe publicatie”. Het was bedoeld om builds op te ruimen die een gecrasht proces had achtergelaten.
In de praktijk betekende het dat een tweede publicatie die midden in een build binnenkwam de eerste als dood verklaarde en zijn eigen build startte. Beide liepen tot het einde tegen dezelfde app. Er was geen slot; er was een stap die de weg actief vrijmaakte voor een race.
Er was ook een tweede, vreemder symptoom bij de allereerste publicatie van een app. Zonder deploymentrecord om te hergebruiken probeerden twee gelijktijdige eerste publicaties er allebei een in te voegen. De database heeft een unieke index op de linknaam van de app (zijn slug, zoals your-app in your-app.mythex.ai), dus de tweede invoeging mislukte. De API vertaalde dat in “die linknaam is al bezet”, en raadde de gebruiker aan een app te hernoemen die op dat moment onder die naam succesvol werd gepubliceerd.
De oplossing: de claim is het slot
Het claimen van het deploymentrecord is nu één voorwaardelijke databaseupdate: neem deze rij en zet hem op bouwen alleen als niemand er op dat moment voor publiceert. Omdat het één statement is, beslist de database over verzoeken die binnenkomen in dezelfde milliseconde. Precies één ervan verandert de rij. De anderen zien dat er nul rijen zijn veranderd, stoppen, en bereiken de orchestrator nooit.
Een slot heeft ook een uitweg nodig wanneer de houder sterft. Als de API midden in een publicatie opnieuw opstartte, zou de rij voor altijd op “bouwen” blijven staan en de app niet meer te publiceren zijn. Dus een rij waarvan de eigenaar geen heartbeats meer stuurt, kan opnieuw worden geclaimd. De lopende publicatie werkt de rij elke 30 seconden bij, en vijf minuten stilte telt als dood. Die drempel komt van de heartbeat, niet van een gok over hoe lang een build duurt. Het bouwen van de eigen Dockerfile van een gebruiker kan legitiem lang duren, en een trage build die nog heartbeat, behoudt zijn claim.
De stap die elke lopende deploy als mislukt markeerde, draait niet meer vóór een publicatie. Alleen rijen die een dood proces heeft achtergelaten, worden opgeruimd.
De verliezer de waarheid vertellen
Het verzoek dat verliest, krijgt nu een duidelijk antwoord: publish_in_progress, samen met de ID van de deployment die loopt. Elke plek die een publicatie kan starten, verwerkt dat:
- De werkruimte volgt de build die al loopt in plaats van een fout over het scherm te tonen.
- De agent krijgt te horen dat er al een publicatie van deze app loopt en dat hij moet wachten, in plaats van dat er iets is mislukt. Dat kon hem eerder aanzetten om een Dockerfile te “fixen” en het opnieuw te proberen.
- Een botsing bij de eerste publicatie op de linknaam wordt gecontroleerd: als de rij die de naam vasthoudt de lopende publicatie van dit project zelf is, is het antwoord “already publishing”, niet “that name is taken”. Een echte botsing met een ander project krijgt nog steeds het hernoemingsbericht.
Hoe we het hebben gecontroleerd
We schreven een reproductiescript dat drie publicaties van één app tegelijk draait tegen een echte database en een stub-orchestrator, en telt hoeveel deploys de orchestrator gevraagd wordt. Het dekt een eerste publicatie en een herpublicatie van een live app. Voor de fix telde het twee deploys. Erna één.
Het script dekt ook de uitweg: het markeert de rij van de app als bouwend zonder heartbeat voor tien minuten, publiceert opnieuw, en controleert dat de nieuwe publicatie de rij claimt en draait.
Unit-tests houden het slot bij de twee dingen die een slot moet doen: van drie gelijktijdige herpublicaties komt er precies één door en krijgen de andere twee te horen dat er een publicatie loopt; en een build waarvan de eigenaar is gestorven, kan opnieuw worden geclaimd. Een andere test controleert dat een nieuwe poging de foutmelding van de vorige poging niet meeneemt.
Wat we hieruit hebben geleerd
- Opruimcode kan stiekem een race zijn. “Alles wat loopt als mislukt markeren, dan starten” klinkt als netjes werken. Met twee aanroepers is het dat elk van hen de ander annuleert.
- Laat de database beslissen. Een read gevolgd door een write laat een gat open voor een ander verzoek. Eén voorwaardelijke update doet dat niet.
- Elk slot heeft een vervaldatum nodig die aan leven is gekoppeld. Zonder die wordt een crash een vastgelopen app.
- Foutmeldingen zijn instructies. “Die naam is bezet” zei mensen het verkeerde te doen. Het juiste bericht was eenvoudiger en waar.
Meer over publiceren in de documentatie: hoe te publiceren. En een verwante fix: je gepubliceerde app blijft online terwijl je hem opnieuw publiceert.