Blog

Zweimal Publish drücken veröffentlicht jetzt nur einmal

Drei Veröffentlichungsanfragen für dieselbe App erreichten unsere API im Abstand von 84 Millisekunden, und jede startete ein vollständiges Deployment dieser App. Sie liefen auf demselben Hosting um die Wette, mehrere der entstehenden Releases schlugen fehl, weil ein anderer schneller war, und das einzige sichtbare Zeichen war eine E-Mail „Release fehlgeschlagen“. Die App selbst lief am Ende. Jetzt startet eine Veröffentlichung, die ankommt, während für dieselbe App bereits eine läuft, keinen zweiten Build. Die Datenbank entscheidet, welche Anfrage gewinnt, und den anderen wird die Wahrheit gesagt: Eine Veröffentlichung läuft bereits.

Was passiert ist

Wenn du eine App auf Mythex veröffentlichst, legt die API ein Deployment für sie an und bittet unseren Orchestrator, also den Dienst, der Builds ausführt, sie zu bauen und freizugeben. Das Veröffentlichen kann über den Publish-Button, über einen anderen Browser-Tab oder über den Agenten gestartet werden, wenn du ihn darum bittest. Ein Doppelklick, zwei Tabs oder eine Agenten-Anfrage, die zweimal ankommt, können alle zu mehr als einer Veröffentlichung derselben App im nahezu gleichen Moment führen.

In diesem Fall kamen drei innerhalb von 84 ms an. Jede startete ein vollständiges Deployment. Auf der Hosting-Plattform wurden Releases derselben App in derselben Sekunde angelegt, und drei davon schlugen fehl, weil die anderen bereits liefen. Ein späteres Release blieb hängen, und die App funktionierte danach.

Das machte es leicht zu übersehen. Der Nutzer sah eine funktionierende App. Die Fehler zeigten sich nur in den E-Mails zu fehlgeschlagenen Releases.

Warum nichts dagegen geschützt hat

Der Veröffentlichungscode hatte tatsächlich einen Schritt, der laufende Deployments des Projekts ansah. Aber er markierte vor dem Start jedes laufende Deployment als fehlgeschlagen, mit der Begründung „durch eine neue Veröffentlichung ersetzt“. Gedacht war das, um Builds aufzuräumen, die ein abgestürzter Prozess zurückgelassen hatte.

In der Praxis hieß das: Eine zweite Veröffentlichung, die mitten im Build ankam, erklärte die erste für tot und startete ihre eigene. Beide liefen bis zum Ende gegen dieselbe App. Es gab kein Lock. Es gab einen Schritt, der aktiv den Weg für ein Rennen freimachte.

Es gab außerdem ein zweites, seltsameres Symptom bei der allerersten Veröffentlichung einer App. Ohne bereits vorhandenen Deployment-Eintrag versuchten zwei gleichzeitige erste Veröffentlichungen, denselben Eintrag einzufügen. Die Datenbank hat einen eindeutigen Index auf dem Link-Namen der App (ihrem Slug, wie your-app in your-app.mythex.ai), also schlug das zweite Einfügen fehl. Die API übersetzte das in „dieser Link-Name ist vergeben“ und riet dem Nutzer, eine App umzubenennen, die in diesem Moment unter diesem Namen erfolgreich veröffentlicht wurde.

Die Lösung: der Anspruch ist das Lock

Das Beanspruchen des Deployment-Eintrags ist jetzt ein einzelnes bedingtes Datenbank-Update: Nimm diese Zeile und markiere sie als im Build nur, wenn niemand sie veröffentlicht. Weil es ein einziges Statement ist, entscheidet die Datenbank über Anfragen, die in derselben Millisekunde ankommen. Genau eine davon ändert die Zeile. Die anderen sehen, dass null Zeilen geändert wurden, brechen ab und erreichen den Orchestrator nie.

Ein Lock braucht außerdem einen Ausweg, wenn sein Halter stirbt. Startete die API neu, während eine Veröffentlichung lief, würde die Zeile für immer als „im Build“ markiert bleiben, und die App ließe sich nicht mehr veröffentlichen. Daher kann eine Zeile, deren Besitzer keine Heartbeats mehr sendet, erneut beansprucht werden. Die laufende Veröffentlichung aktualisiert die Zeile alle 30 Sekunden, und fünf Minuten Stille gelten als tot. Dieser Schwellenwert kommt aus dem Heartbeat und nicht aus einer Schätzung, wie lange ein Build dauern darf. Das Bauen eines eigenen Dockerfiles des Nutzers kann legitim lange dauern, und ein langsamer Build, der weiter Heartbeats sendet, behält seinen Anspruch.

Der Schritt, der jedes laufende Deployment als fehlgeschlagen markierte, läuft vor einer Veröffentlichung nicht mehr. Bereinigt werden nur noch Zeilen, die ein toter Prozess zurückgelassen hat.

Dem Verlierer die Wahrheit sagen

Die Anfrage, die verliert, bekommt jetzt eine klare Antwort: publish_in_progress, zusammen mit der ID des laufenden Deployments. Jede Stelle, die eine Veröffentlichung starten kann, behandelt das:

  • Der Workspace folgt dem Build, der bereits läuft, statt über ihn eine Fehlermeldung zu legen.
  • Der Agent erfährt, dass eine Veröffentlichung dieser App bereits läuft, und soll auf sie warten. Früher wurde ihm gesagt, etwas sei fehlgeschlagen, was ihn dazu bringen konnte, ein Dockerfile „zu reparieren“ und es erneut zu versuchen.
  • Eine Kollision bei der ersten Veröffentlichung auf dem Link-Namen wird geprüft: Hält die Zeile mit dem Namen die laufende Veröffentlichung dieses eigenen Projekts, lautet die Antwort „wird bereits veröffentlicht“, nicht „Name ist vergeben“. Eine echte Kollision mit einem anderen Projekt erhält weiterhin die Meldung zum Umbenennen.

Wie wir es geprüft haben

Wir haben ein Reproduktionsskript geschrieben, das drei Veröffentlichungen derselben App gleichzeitig gegen eine echte Datenbank und einen Stub-Orchestrator ausführt und zählt, wie viele Deployments der Orchestrator angefordert bekommt. Es deckt eine erste Veröffentlichung und eine Neuveröffentlichung einer live laufenden App ab. Vor der Änderung zählte es zwei Deployments, danach eines.

Das Skript deckt außerdem den Notausgang ab: Es markiert die Zeile der App als im Build ohne Heartbeat für zehn Minuten, veröffentlicht erneut und prüft, dass die neue Veröffentlichung die Zeile beansprucht und läuft.

Unit-Tests binden das Lock an die zwei Dinge, die ein Lock leisten muss: Von drei gleichzeitigen Neuveröffentlichungen kommt genau eine durch, und die beiden anderen erfahren, dass eine Veröffentlichung läuft. Und ein Build, dessen Besitzer gestorben ist, kann beansprucht werden. Ein weiterer Test prüft, dass ein neuer Versuch die Fehlermeldung des vorherigen Versuchs nicht übernimmt.

Was wir daraus gelernt haben

  • Aufräumcode kann ein verkapptes Rennen sein. „Alles Laufende fehlschlagen lassen und dann starten“ klingt nach Hygiene. Mit zwei Aufrufern bricht jeder den anderen ab.
  • Lass die Datenbank entscheiden. Ein Lesen gefolgt von einem Schreiben lässt eine Lücke für eine andere Anfrage. Ein einzelnes bedingtes Update nicht.
  • Jedes Lock braucht ein Verfallsdatum, das an die Lebendigkeit gebunden ist. Ohne eines wird aus einem Absturz eine feststeckende App.
  • Fehlermeldungen sind Anweisungen. „Dieser Name ist vergeben“ sagte den Leuten, sie sollten das Falsche tun. Die richtige Meldung war einfacher und wahr.

Mehr zum Veröffentlichen steht in der Dokumentation: So veröffentlichst du. Und eine verwandte Korrektur: Deine veröffentlichte App bleibt online, während du sie neu veröffentlichst.

Mythex · Pricing · Alles mit KI bauen · Vorlagen für Websites und Apps · Mythex im Vergleich · Glossar zum App-Bau mit KI · Kostenlose Tools für den App-Bau mit KI · English

English አማርኛ العربية Български বাংলা Bosanski Català Čeština Dansk Ελληνικά Español Español (España) Eesti فارسی Suomi Français Français (Canada) ગુજરાતી हिन्दी Hrvatski Magyar Հայերեն Bahasa Indonesia Íslenska Italiano 日本語 ქართული Қазақ тілі ಕನ್ನಡ 한국어 Lietuvių Latviešu Македонски മലയാളം Монгол मराठी Bahasa Melayu မြန်မာ Norsk bokmål Nederlands ਪੰਜਾਬੀ Polski Português (Brasil) Português (Portugal) Română Русский Slovenčina Slovenščina Soomaali Shqip Srpski Svenska Kiswahili தமிழ் తెలుగు ไทย Tagalog Türkçe Українська اردو Tiếng Việt 简体中文 繁體中文(香港) 繁體中文(台灣)