Steven
Steven6 Min. Lesezeit

Eine abgebrochene Verbindung sollte nicht die ganze App zum Absturz bringen — unsere tat es

Als unser Backend mitten in einem Meeting offline ging, hat es nicht nur die Transkription pausiert — es hat die ganze App zum Absturz gebracht. Die Ursache war ein einziges nicht behandeltes Event, und der Fix waren zehn Zeilen. Das ist das Release-Cluster, das GeekBye angemeldet bleiben und verbunden bleiben ließ, durch genau die Dinge, die es früher umgebracht haben.

Engineering
Zuverlässigkeit
Electron
GeekBye-Releases
Eine abgebrochene Verbindung sollte nicht die ganze App zum Absturz bringen — unsere tat es

Manche Bugs unterbrechen ein Feature. Die schlimmsten reißen alles um sich herum mit. In einem frühen GeekBye-Build hörte die Transkription, wenn unser Backend ausfiel, während du mitten in einem Meeting warst, nicht nur auf — die ganze App stürzte ab. Eine abgebrochene Verbindung, und das ganze Fenster war weg.

Das Release-Cluster, das das behoben hat — v1.6.8 bis v1.6.11 — ist eine Meisterklasse in der unglamourösen Arbeit von "angemeldet bleiben, verbunden bleiben". Der Haupt-Fix waren zehn Zeilen. Hier ist, was diese zehn Zeilen waren, und alles andere, das daneben ausgeliefert wurde.

Zehn Zeilen zwischen einer Trennung und einem Absturz

Der Absturz lebte im Audio-Code. Wenn eine Transkriptionssitzung ihre WebSockets abbaute, rief sie removeAllListeners() auf ihnen auf — vernünftiges Aufräumen. Aber hier ist das Node.js-Detail, das Aufräumen in eine Katastrophe verwandelt: ein error-Event ohne Listener wird nicht ignoriert. Es wird als nicht abgefangene Exception erneut geworfen.

Stell dir also die Abfolge vor, wenn das Backend offline geht: Der Socket hat ein error-Event in der Warteschlange, bereit zu feuern. Das Aufräumen führt removeAllListeners() aus und entfernt den Handler, der es abgefangen hätte. Einen Moment später feuert der eingereihte Fehler — ins Leere. Node sieht ein error-Event, bei dem niemand zuhört, und tut das Einzige, was sein Vertrag erlaubt: Es wirft es als nicht abgefangene Exception, die den Prozess zum Absturz bringt. Das Backend des Nutzers hatte einen Zwei-Sekunden-Schluckauf, und seine App war verschwunden.

Der Fix (v1.6.8) ist fast antiklimaktisch. Direkt nach dem Entfernen der Listener einen bewussten No-op-Fehler-Handler wieder anhängen:

this.micWebSocket.removeAllListeners()
this.micWebSocket.on('error', () => {}) // absorb late error events

Diese leere Funktion ist der ganze Punkt. Sie gibt dem anstehenden Fehler einen Ort zum Landen — eine Senke — sodass Node einen Listener hat und niemals erneut wirft. Derselbe Commit räumte auch halboffene Sockets auf, wenn ein Verbindungsversuch auf halbem Weg scheitert, damit ein misslungener Connect keinen lebenden Socket mit einem wartenden Fehler baumeln lassen kann. Der Commit-Kommentar sagt es unverblümt: "Ohne das werden anstehende Fehler zu nicht abgefangenen Exceptions und bringen die App zum Absturz." Zehn Zeilen verwandelten einen kompletten Absturz in eine stille, wiederherstellbare Trennung.

Die Lektion ist mehr wert als der Fix: Jedes Mal, wenn du removeAllListeners() auf einem Socket oder Stream aufrufst, den du vielleicht noch abbaust, hänge zuerst eine No-op-error-Senke wieder an. Ein unbehandeltes error-Event ist ein Absturz, keine Log-Zeile.

Angemeldet bleiben: im Voraus refreshen und beim Aufprall refreshen

Die "verbunden bleiben"-Hälfte hatte einen "angemeldet bleiben"-Zwilling. Vor diesem Cluster lief GeekByes Auth-Token einfach ab — nach einer Weile wurdest du ohne Umstände zurück zum Login-Bildschirm geworfen, mitten im Workflow, aus keinem für dich sichtbaren Grund.

v1.6.8 behob das aus beiden Richtungen:

  • Proaktiv: Ein Timer refresht das Token, bevor es abläuft, geplant auf den Ablauf minus einen intelligenten Puffer (einen Bruchteil der Token-Lebensdauer, mit Unter- und Obergrenze). Du wirst refresht, bevor du es je bemerken würdest.
  • Reaktiv: Wenn eine Anfrage trotzdem mit 401 zurückkommt — Uhr-Drift, ein verpasster Timer, ein kalter Laptop — refresht der Client einmal und wiederholt die ursprüngliche Anfrage genau einmal. Ein einzelnes Guard-Flag deckelt diesen Retry, sodass ein hartnäckiger 401 niemals in eine Endlosschleife drehen kann.

Der feine, aber entscheidende Teil: Beide Pfade liegen hinter einem Mutex. Wenn zehn Anfragen im selben Augenblick einen 401 treffen, feuern sie nicht zehn Refreshes — sie warten alle auf das eine laufende Refresh und wiederholen dann. Und wenn ein Refresh wirklich nicht gerettet werden kann, lässt die App die Sitzung elegant ablaufen — ein klarer Zehn-Sekunden-Hinweis "deine Sitzung ist abgelaufen, bitte melde dich erneut an" — statt eines stillen Tritts zum Login-Bildschirm.

Reconnect: im Client hinzufügen, dann ins Backend verschieben

v1.6.9 machte eine abgebrochene Transkriptionsverbindung wiederherstellbar statt fatal. Wenn das Backend meldete, dass die vorgelagerte Sprachverbindung abgebrochen war, hörte der Client auf, einen fatalen Fehler anzuzeigen, und verband sich stattdessen neu — mit bewusst ruhiger UX: ein einzelner bernsteinfarbener "Reconnecting…"-Hinweis (nicht einer pro Versuch), ein grüner "Reconnected"-Hinweis, wenn sie zurückkam, und der finale "bitte neu starten"-Fehler erst, nachdem jeder Versuch erschöpft war.

Dann etwas, das ich an diesem Cluster mag: ein Release später, in v1.6.10, haben wir diese clientseitige Reconnect-Logik gelöscht — etwa 113 Zeilen — und den Reconnect ins Backend verschoben, das jetzt leise "reconnecting"-Statusmeldungen aussendet, die der Client nur spiegelt. Wir bauten die Wiederherstellung im Client, entschieden, dass das Backend der richtige Eigentümer war, und verschoben sie. Das ist kein Herumeiern; das ist der ehrliche Lebenszyklus eines schweren Problems. Über beide Enden einer Verbindung verteilte Zuverlässigkeitslogik ist ein Geruch — wähle einen Eigentümer. (Das Backend wurde schließlich der endgültige Eigentümer davon, was die Reconnect-Geschichte ist, die in warum dein KI-Notizschreiber bei schlechtem WLAN stoppt erzählt wird.)

Dasselbe Release machte die Aufnahmezeit-Limits menschlich: statt eines rohen Fehlers ein Warnhinweis, der die Aufnahme weiterlaufen lässt, und eine klare "Aufnahmelimit erreicht"-Meldung, wenn sie tatsächlich stoppt — der freundliche Satz sichtbar, das interne Präfix entfernt.

Ein Retry, sie alle zu beherrschen

Bis v1.6.11 war dieselbe Retry-mit-Backoff-Logik an drei verschiedenen Stellen zusammenkopiert worden — Auth, Updates, Sitzungsabbau. Also wurde sie in ein einziges Modul mit einer geteilten Regel darüber konsolidiert, was überhaupt einen Retry wert ist: transiente Netzwerkfehler (Verbindung zurückgesetzt, Timeout, DNS-Schluckauf, aufgelegter Socket) werden mit exponentiellem Backoff wiederholt; ein echter Serverfehler schlägt schnell fehl, denn einen echten 4xx zu wiederholen verschwendet nur die Zeit des Nutzers. Ein Klassifizierer, ein paar benannte Presets, drei Aufrufer darauf migriert.

Was dieses Cluster gelehrt hat

  1. Ein unbehandeltes error-Event ist ein Absturz, keine Log-Zeile. Node wirft error-Events, die keinen Listener haben, erneut. Entferne die Listener von einem Socket, den du vielleicht noch abbaust, und du musst eine No-op-Fehlersenke wieder anhängen — sonst reißt eine abgebrochene Verbindung die ganze App mit.
  2. Refreshe die Anmeldedaten vor dem Ablauf und reaktiv bei einem 401 — mit einem Mutex auf beiden. Proaktiv hält dich angemeldet; reaktiv fängt die Randfälle ab; der Mutex bedeutet, dass N gleichzeitige Aufrufer genau ein Refresh verursachen, und ein Retry-Deckel bedeutet, dass ein schlechtes Token nicht ewig kreisen kann.
  3. Wiederhole nur bei transienten Fehlern und zentralisiere den Klassifizierer. "Ist das einen Retry wert?" ist eine einzige Entscheidung, die an einen einzigen Ort gehört. Einen echten Fehler zu wiederholen ist nur ein langsameres Scheitern.
  4. Entscheide, wem der Reconnect gehört. Wir fügten ihn im Client hinzu, dann verschoben wir ihn ins Backend. Wiederherstellungslogik, die auf beiden Enden lebt, ist ein Bug-Generator; gib ihr ein einziges Zuhause.

Das ist das dritte Kapitel der Zuverlässigkeitsgeschichte, die zu GeekBye v2 wird. Für das vorherige Kapitel siehe wir haben 5.000 Zeilen Audio-Code gelöscht und die Transkripte tauchten doppelt auf (v1.6.0); für den Ort, an dem sich der Reconnect schließlich niederließ, warum dein KI-Notizschreiber bei schlechtem WLAN stoppt; und für den gesamten Bogen die Anatomie, Software bis zur Perfektion auszuliefern.

Ähnliche Artikel

Die Sicherheitsprüfung, die unsere App unmöglich zu beenden machte
Steven
Steven6 Min. Lesezeit

Die Sicherheitsprüfung, die unsere App unmöglich zu beenden machte

Das automatische Update war das schwerste Feature, das wir je ausgeliefert haben — sechs Releases in vier Tagen, damit es die App nicht mehr unbrauchbar machte. Der schlimmste Bug war einer, den wir aus lauter Vorsicht selbst eingebaut haben: eine 500-Millisekunden-„Sicherheits"-Prüfung, die aus einem fehlgeschlagenen Update einen Prozess machte, den man buchstäblich nicht beenden konnte.

Engineering
Electron
Zuverlässigkeit
Was eine Version 2 wirklich kostet: 206 Commits ehrlicher Zustände
Steven
Steven7 Min. Lesezeit

Was eine Version 2 wirklich kostet: 206 Commits ehrlicher Zustände

Die v2 von GeekBye war kein Feature-Release. Es waren 206 Commits, die auf eine einzige Idee zielten: Die App darf niemals über ihren eigenen Zustand lügen. Das ist der Preis dafür — inklusive des Ein-Zeilen-Fehlers im Lockfile, der uns fast daran gehindert hätte, überhaupt etwas auszuliefern.

Zuverlässigkeit
Engineering
Release
Eine CSS-Variable, fünf Review-Runden und eine Swift-Toolchain, die log
Steven
Steven7 Min. Lesezeit

Eine CSS-Variable, fünf Review-Runden und eine Swift-Toolchain, die log

GeekBye v2.0.7 machte das gesamte Overlay verstellbar durchscheinend — was nach einer einzeiligen CSS-Änderung klingt und definitiv keine war. Die eigentliche Geschichte ist, was das Code-Review aufdeckte: zwei Flächen, die sich weigerten zu verblassen, eine Aufnahmeleiste, die bei gleicher Deckkraft falsch aussah, und ein kompiliertes Binary, das um 672 Bytes hin und her sprang, weil unsere eigene Dokumentation einem Reviewer die falsche Swift-Version nannte.

Engineering
Design
Build