Steven
Steven7 min de lectura

Una connexió perduda no hauria de fer petar tota l'aplicació — però la nostra ho feia

Quan el nostre backend es va desconnectar en plena reunió, no només va aturar la transcripció: va fer petar tota l'aplicació. La causa era un únic esdeveniment sense gestionar, i la solució eren deu línies. Aquest és el grup de versions que va fer que GeekBye es mantingués amb la sessió iniciada i connectat a través de les coses que abans el mataven.

Enginyeria
Fiabilitat
Electron
GeekBye Releases
Una connexió perduda no hauria de fer petar tota l'aplicació — però la nostra ho feia

Alguns bugs interrompen una funció. Els pitjors s''enduen tot el que hi ha al voltant. En una versió primerenca de GeekBye, si el nostre backend es desconnectava mentre eres en plena reunió, la transcripció no només s''aturava: tota l''aplicació petava. Una connexió perduda, i tota la finestra desapareixia.

El grup de versions que ho va solucionar — de la v1.6.8 a la v1.6.11 — és una classe magistral de la feina poc glamurosa de "mantén la sessió, mantén la connexió". La solució protagonista eren deu línies. Aquí tens quines eren aquelles deu línies, i tota la resta que es va publicar al costat.

Deu línies entre una desconnexió i una fallada

La fallada vivia al codi d''àudio. Quan una sessió de transcripció desmuntava els seus WebSockets, hi cridava removeAllListeners() — una neteja raonable. Però aquí hi ha el detall de Node.js que converteix la neteja en catàstrofe: un esdeveniment error sense oient no s''ignora. Es torna a llançar com una excepció no capturada.

Així que imagina''t la seqüència quan el backend es desconnecta: el socket té un esdeveniment error a la cua a punt de disparar-se. La neteja executa removeAllListeners(), treient el gestor que l''hauria capturat. Un moment després, l''error a la cua es dispara — cap al buit. Node veu un esdeveniment error que ningú no escolta, i fa l''única cosa que el seu contracte permet: el llança com una excepció no capturada, que fa petar el procés. El backend de l''usuari va tenir un ensopec de dos segons i la seva aplicació es va esfumar.

La solució (v1.6.8) és gairebé anticlimàtica. Just després de treure els oients, torna a enganxar deliberadament un gestor d''errors no-op:

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

Aquesta funció buida és tot el punt. Dona a l''error pendent un lloc on aterrar — un absorbidor — de manera que Node té un oient i mai no torna a llançar. El mateix commit també va netejar els sockets mig oberts quan un intent de connexió falla a mitges, perquè una connexió fracassada no pugui deixar un socket viu penjant amb un error a punt de disparar-se. El comentari del commit ho diu clarament: "Sense això, els errors pendents es converteixen en excepcions no capturades i fan petar l''aplicació." Deu línies van convertir una fallada total en una desconnexió silenciosa i recuperable.

La lliçó val més que la solució: cada vegada que crides removeAllListeners() en un socket o stream que potser encara estàs desmuntant, torna a enganxar primer un absorbidor error no-op. Un esdeveniment error sense gestionar és una fallada, no una línia de registre.

Mantenir la sessió: renova per avançat, i renova al rebot

La meitat de "mantén la connexió" tenia un bessó de "mantén la sessió". Abans d''aquest grup, el token d''autenticació de GeekBye simplement caducava — al cap d''una estona et retornaven sense contemplacions a la pantalla d''inici de sessió, enmig del teu flux de treball, sense cap raó que poguessis veure.

La v1.6.8 ho va solucionar des de totes dues direccions:

  • Proactiu: un temporitzador renova el token abans que caduqui, programat a la caducitat menys un marge intel·ligent (una fracció de la vida útil del token, amb un mínim i un màxim). Ja estàs renovat abans que te''n adonessis mai.
  • Reactiu: si tot i així una sol·licitud torna com a 401 — desfasament de rellotge, un temporitzador perdut, un portàtil fred — el client renova un cop i reintenta la sol·licitud original exactament una vegada. Una única bandera de guàrdia limita aquest reintent perquè un 401 persistent mai no pugui derivar en un bucle infinit.

La part subtil però crítica: tots dos camins estan darrere d''un mutex. Si deu sol·licituds reben un 401 al mateix instant, no disparen deu renovacions — totes esperen la única renovació en curs i després reintenten. I quan una renovació realment no es pot salvar, l''aplicació fa caducar la sessió de manera suau — una notificació clara de deu segons "la teva sessió ha caducat, torna a iniciar la sessió" — en lloc d''una expulsió silenciosa cap a la pantalla d''inici de sessió.

Reconnexió: afegeix-la al client, després trasllada-la al backend

La v1.6.9 va fer que una connexió de transcripció perduda fos recuperable en lloc de fatal. Quan el backend informava que la connexió de veu de dalt havia caigut, el client deixava de mostrar un error fatal i, en canvi, es reconnectava — amb una UX deliberadament calmada: una única notificació ambre "S''està reconnectant…" (no una per intent), una notificació verda "Reconnectat" quan tornava, i l''error terminal "torna a iniciar" només després que cada intent s''hagués esgotat.

Després, una cosa que m''agrada d''aquest grup: una versió més tard, a la v1.6.10, vam eliminar aquella lògica de reconnexió del costat del client — unes 113 línies — i vam traslladar la reconnexió al backend, que ara emet missatges d''estat tranquils de "s''està reconnectant" que el client simplement reflecteix. Vam construir la recuperació al client, vam decidir que el backend era el propietari correcte, i la vam traslladar. Això no és titubejar; és el cicle de vida honest d''un problema difícil. Tenir la lògica de fiabilitat repartida entre tots dos extrems d''una connexió fa mala olor — tria un propietari. (El backend va acabar sent el propietari definitiu d''això, que és la història de reconnexió explicada a per què el teu prenedor de notes amb IA s''atura amb mala wifi.)

La mateixa versió va fer humans els límits de temps d''enregistrament: en lloc d''un error cru, una notificació d''advertència que deixa continuar l''enregistrament, i un missatge clar "S''ha assolit el límit d''enregistrament" quan realment s''atura — la frase amable a la superfície, el prefix intern retirat.

Un reintent per governar-los tots

Per la v1.6.11, la mateixa lògica de reintent amb backoff s''havia copiat i enganxat en tres llocs diferents — autenticació, actualitzacions, desmuntatge de sessió. Així que es va consolidar en un únic mòdul amb una única regla compartida sobre què val la pena reintentar: les fallades de xarxa transitòries (connexió reiniciada, temps esgotat, ensopec de DNS, socket penjat) es reintenten amb backoff exponencial; un error de servidor real falla ràpid, perquè reintentar un 4xx genuí només malbarata el temps de l''usuari. Un classificador, uns quants presets amb nom, tres sol·licitants migrats a sobre.

Què ens va ensenyar aquest grup

  1. Un esdeveniment error sense gestionar és una fallada, no una línia de registre. Node torna a llançar els esdeveniments error que no tenen oient. Treu els oients d''un socket que potser encara estàs desmuntant, i has de tornar a enganxar un absorbidor d''errors no-op — o una connexió perduda s''endú tota l''aplicació.
  2. Renova les credencials abans de la caducitat i reactivament davant d''un 401 — amb un mutex a totes dues. El proactiu et manté amb la sessió iniciada; el reactiu captura els casos límit; el mutex significa que N sol·licitants simultanis provoquen exactament una renovació, i un límit de reintents significa que un token dolent no pot fer bucle per sempre.
  3. Reintenta només davant d''errors transitoris, i centralitza el classificador. "Val la pena reintentar això?" és una decisió que pertany a un sol lloc. Reintentar un error real és només una fallada més lenta.
  4. Decideix qui és el propietari de la reconnexió. La vam afegir al client, després la vam traslladar al backend. La lògica de recuperació que viu a tots dos extrems és un generador de bugs; dona-li una única llar.

Aquest és el tercer capítol de la història de fiabilitat que esdevé GeekBye v2. Per al capítol anterior, mira vam esborrar 5.000 línies de codi d''àudio i les transcripcions van començar a aparèixer dues vegades (v1.6.0); per veure on va acabar assentant-se la reconnexió, per què el teu prenedor de notes amb IA s''atura amb mala wifi; i per a tot l''arc, l''anatomia de publicar programari fins a la perfecció.