
O conexiune pierdută n-ar trebui să-ți prăbușească toată aplicația — dar pe a noastră o făcea
Când backend-ul nostru a picat în plină ședință, nu doar că a oprit transcrierea — a prăbușit întreaga aplicație. Cauza era un singur eveniment netratat, iar reparația a fost de zece linii. Acesta e grupul de lansări care a făcut GeekBye să rămână autentificat și conectat prin lucrurile care înainte îl omorau.
Unele bug-uri întrerup o funcție. Cele mai rele dărâmă tot ce e în jurul ei. Într-un build timpuriu de GeekBye, dacă backend-ul nostru pica în timp ce erai în plină ședință, transcrierea nu doar se oprea — întreaga aplicație se prăbușea. O singură conexiune pierdută și toată fereastra dispărea.
Grupul de lansări care a reparat asta — v1.6.8 până la v1.6.11 — e o lecție de maestru în munca lipsită de glamour de „rămâi autentificat, rămâi conectat". Reparația emblematică a avut zece linii. Iată care erau acele zece linii și tot ce s-a livrat alături de ele.
Zece linii între o deconectare și o prăbușire
Prăbușirea trăia în codul audio. Când o sesiune de transcriere își demonta WebSocket-urile, apela removeAllListeners() pe ele — o curățenie rezonabilă. Dar iată detaliul din Node.js care transformă curățenia în catastrofă: un eveniment error fără niciun ascultător nu e ignorat. E re-aruncat ca excepție neprinsă.
Așa că imaginează-ți secvența când backend-ul pică: socket-ul are un eveniment error în coadă, gata să se declanșeze. Curățenia rulează removeAllListeners(), smulgând handler-ul care l-ar fi prins. O clipă mai târziu eroarea din coadă se declanșează — în gol. Node vede un eveniment error fără ca nimeni să asculte și face singurul lucru pe care i-l permite contractul: îl aruncă drept excepție neprinsă, ceea ce prăbușește procesul. Backend-ul utilizatorului a sughițat două secunde și aplicația lor a dispărut.
Reparația (v1.6.8) e aproape anticlimactică. Imediat după ce cureți ascultătorii, reatașezi un handler error no-op deliberat:
this.micWebSocket.removeAllListeners()
this.micWebSocket.on('error', () => {}) // absorb late error events
Acea funcție goală e tot rostul. Îi oferă erorii în așteptare un loc unde să aterizeze — un colector — astfel încât Node are un ascultător și nu re-aruncă niciodată. Același commit a curățat și socket-urile pe jumătate deschise când o încercare de conexiune eșuează la mijloc, ca un connect ratat să nu poată lăsa un socket viu atârnând cu o eroare gata să se declanșeze. Comentariul commit-ului o spune limpede: „Fără asta, erorile în așteptare devin excepții neprinse și prăbușesc aplicația." Zece linii au transformat o prăbușire completă într-o deconectare tăcută și recuperabilă.
Lecția valorează mai mult decât reparația: oricând apelezi removeAllListeners() pe un socket sau stream pe care s-ar putea să încă îl demontezi, reatașează mai întâi un colector de erori error no-op. Un eveniment error netratat e o prăbușire, nu o linie de log.
Să rămâi autentificat: reîmprospătează înainte și reîmprospătează la ricoșeu
Jumătatea „rămâi conectat" avea un geamăn „rămâi autentificat". Înainte de acest grup, token-ul de autentificare al GeekBye pur și simplu expira — după o vreme erai aruncat fără ceremonie înapoi la ecranul de login, în plin flux de lucru, fără vreun motiv pe care să-l poți vedea.
v1.6.8 a reparat asta din ambele direcții:
- Proactiv: un timer reîmprospătează token-ul înainte să expire, programat la expirare minus un buffer inteligent (o fracțiune din durata de viață a token-ului, cu prag minim și plafon). Ești reîmprospătat înainte să observi vreodată.
- Reactiv: dacă o cerere se întoarce totuși cu
401— derapaj de ceas, un timer ratat, un laptop rece — clientul reîmprospătează o dată și reîncearcă cererea originală exact o dată. Un singur flag de gardă limitează acea reîncercare, astfel încât un 401 persistent să nu poată intra niciodată într-o buclă infinită.
Partea subtilă, dar critică: ambele căi sunt în spatele unui mutex. Dacă zece cereri lovesc un 401 în același instant, nu declanșează zece reîmprospătări — toate așteaptă singura reîmprospătare în curs și apoi reîncearcă. Iar când o reîmprospătare chiar nu poate fi salvată, aplicația expiră sesiunea grațios — o notificare clară de zece secunde „sesiunea ta a expirat, te rugăm să te autentifici din nou" — în loc de un șut tăcut către ecranul de login.
Reconectarea: adaug-o pe client, apoi mut-o pe backend
v1.6.9 a făcut ca o conexiune de transcriere pierdută să fie recuperabilă în loc de fatală. Când backend-ul raporta că s-a pierdut conexiunea de voce din amonte, clientul a încetat să afișeze o eroare fatală și în schimb se reconecta — cu un UX deliberat calm: o singură notificare chihlimbarie „Se reconectează…" (nu una per încercare), o notificare verde „Reconectat" când revenea și eroarea terminală „te rugăm repornește" doar după ce fiecare încercare era epuizată.
Apoi ceva ce-mi place la acest grup: o lansare mai târziu, în v1.6.10, am șters acea logică de reconectare de pe client — cam 113 linii — și am mutat reconectarea pe backend, care acum emite mesaje tăcute de stare „se reconectează" pe care clientul doar le reflectă. Am construit recuperarea pe client, am decis că backend-ul e proprietarul potrivit și am mutat-o. Nu-i ezitare; e ciclul de viață onest al unei probleme grele. Logica de fiabilitate împărțită între ambele capete ale unei conexiuni e un miros urât — alege un proprietar. (Backend-ul a devenit în cele din urmă proprietarul definitiv al acestui lucru, ceea ce e povestea reconectării spusă în de ce notetaker-ul tău AI se oprește pe Wi-Fi prost.)
Aceeași lansare a făcut umane limitele de timp la înregistrare: în loc de o eroare brută, o notificare de avertizare care lasă înregistrarea să continue și un mesaj clar „Limită de înregistrare atinsă" când chiar se oprește — propoziția prietenoasă adusă la suprafață, prefixul intern eliminat.
O singură reîncercare care le stăpânește pe toate
Până la v1.6.11, aceeași logică de reîncercare-cu-backoff fusese copiată-lipită în trei locuri diferite — autentificare, actualizări, demontarea sesiunii. Așa că a fost consolidată într-un singur modul cu o singură regulă comună despre ce merită măcar reîncercat: eșecurile pasagere de rețea (resetare de conexiune, timeout, sughiț DNS, socket căzut) sunt reîncercate cu backoff exponențial; o eroare reală de server eșuează rapid, pentru că a reîncerca un 4xx autentic doar irosește timpul utilizatorului. Un clasificator, câteva preseturi denumite, trei apelanți migrați pe el.
Ce ne-a învățat acest grup
- Un eveniment
errornetratat e o prăbușire, nu o linie de log. Node re-aruncă evenimentele error care nu au niciun ascultător. Curăță ascultătorii pe un socket pe care s-ar putea să încă îl demontezi și trebuie să reatașezi un colector de erori no-op — altfel o singură conexiune pierdută ia toată aplicația cu ea. - Reîmprospătează credențialele înainte de expirare și reactiv la un 401 — cu un mutex pe ambele. Proactivul te ține autentificat; reactivul prinde cazurile marginale; mutex-ul înseamnă că N apelanți simultani cauzează exact o singură reîmprospătare, iar un plafon de reîncercare înseamnă că un token stricat nu poate bucla la nesfârșit.
- Reîncearcă doar la erori pasagere și centralizează clasificatorul. „Merită asta reîncercat?" e o singură decizie care aparține unui singur loc. A reîncerca o eroare reală e doar un eșec mai lent.
- Decide cine deține reconectarea. Am adăugat-o pe client, apoi am mutat-o pe backend. Logica de recuperare care trăiește pe ambele capete e un generator de bug-uri; dă-i o singură casă.
Acesta e al treilea capitol al poveștii de fiabilitate care devine GeekBye v2. Pentru capitolul anterior, vezi am șters 5.000 de linii de cod audio și transcrierile au început să apară de două ori (v1.6.0); pentru locul unde a ajuns în cele din urmă reconectarea, de ce notetaker-ul tău AI se oprește pe Wi-Fi prost; iar pentru întregul arc, anatomia livrării software-ului la perfecțiune.