
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.
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
401zurü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
- 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. - 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.
- 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.
- 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.