
Egy megszakadt kapcsolatnak nem szabadna összeomlasztania az egész alkalmazást — de a miénk ezt tette
Amikor a backendünk értekezlet közben elesett, nem csak a leiratozást állította le — összeomlasztotta az egész alkalmazást. Az ok egyetlen kezeletlen esemény volt, a javítás pedig tíz sor. Ez az a kiadási csokor, amely elérte, hogy a GeekBye bejelentkezve és kapcsolódva maradjon azokon a dolgokon át, amelyek korábban megölték.
Egyes hibák megszakítanak egy funkciót. A legrosszabbak mindent ledöntenek körülötte. A GeekBye egy korai buildjében, ha a backendünk elesett, miközben épp értekezlet közepén voltál, a leiratozás nem egyszerűen leállt — az egész alkalmazás összeomlott. Egyetlen megszakadt kapcsolat, és az egész ablak eltűnt.
Az a kiadási csokor, amely ezt megjavította — a v1.6.8-tól a v1.6.11-ig — mesteri lecke a „maradj bejelentkezve, maradj kapcsolódva" hálátlan munkájában. A vezető javítás tíz sor volt. Íme, mi volt az a tíz sor, és minden más, ami mellettük megjelent.
Tíz sor egy szétkapcsolódás és egy összeomlás között
Az összeomlás az audiokódban élt. Amikor egy leiratozási munkamenet lebontotta a WebSocketjeit, meghívta rajtuk a removeAllListeners()-t — ésszerű takarítás. De itt van a Node.js-részlet, amely a takarítást katasztrófává változtatja: egy error eseményt, amelynek nincs hallgatója, nem hagyunk figyelmen kívül. Elkapatlan kivételként dobódik újra.
Képzeld hát el a sorrendet, amikor a backend elesik: a socketnek van egy error eseménye a sorban, készen a kiváltásra. A takarítás lefuttatja a removeAllListeners()-t, letépve a kezelőt, amely elkapta volna. Egy pillanattal később a sorba állított hiba kiváltódik — az ürességbe. A Node lát egy error eseményt, amelyet senki sem hallgat, és megteszi az egyetlen dolgot, amit a szerződése megenged: elkapatlan kivételként dobja, ami összeomlasztja a folyamatot. A felhasználó backendje két másodpercre megcsuklott, és az alkalmazásuk eltűnt.
A javítás (v1.6.8) szinte antiklimaktikus. Közvetlenül a hallgatók eltávolítása után csatolj újra egy szándékos no-op error kezelőt:
this.micWebSocket.removeAllListeners()
this.micWebSocket.on('error', () => {}) // absorb late error events
Az az üres függvény a lényeg maga. Ad a függőben lévő hibának egy helyet, ahová leszállhat — egy elnyelőt —, így a Node-nak van egy hallgatója, és soha nem dob újra. Ugyanaz a commit feltakarította a félig nyitott socketeket is, amikor egy csatlakozási kísérlet félúton meghiúsul, hogy egy elrontott connect ne hagyhasson egy élő socketet lógva egy kiváltásra kész hibával. A commit megjegyzése világosan kimondja: „Enélkül a függőben lévő hibák elkapatlan kivételekké válnak és összeomlasztják az alkalmazást." Tíz sor egy teljes összeomlást csendes, helyreállítható szétkapcsolódássá változtatott.
A tanulság többet ér, mint a javítás: valahányszor meghívod a removeAllListeners()-t egy socketen vagy streamen, amelyet talán még lebontasz, előbb csatolj újra egy no-op error elnyelőt. Egy kezeletlen error esemény összeomlás, nem egy naplósor.
Bejelentkezve maradni: frissíts előre, és frissíts a visszapattanáskor
A „maradj kapcsolódva" felének volt egy „maradj bejelentkezve" ikertestvére. E csokor előtt a GeekBye hitelesítési tokenje egyszerűen lejárt — egy idő után ceremónia nélkül visszavágott a bejelentkezési képernyőre, a munkafolyamat közepén, minden látható ok nélkül.
A v1.6.8 ezt mindkét irányból megjavította:
- Proaktívan: egy időzítő a lejárat előtt frissíti a tokent, a lejáratra mínusz egy okos pufferre ütemezve (a token élettartamának egy töredéke, alsó és felső korláttal). Már azelőtt frissülsz, hogy valaha észrevennéd.
- Reaktívan: ha egy kérés mégis
401-gyel tér vissza — óracsúszás, egy elmulasztott időzítő, egy hideg laptop —, a kliens egyszer frissít, és pontosan egyszer újrapróbálja az eredeti kérést. Egyetlen őrjelző megszabja ezt az újrapróbálkozást, így egy tartós 401 soha nem pöröghet végtelen ciklusba.
A finom, de kritikus rész: mindkét út egy mutex mögött van. Ha tíz kérés ugyanabban a pillanatban ütközik egy 401-be, nem indít tíz frissítést — mind az egyetlen folyamatban lévő frissítésre várnak, majd újrapróbálnak. És amikor egy frissítést tényleg nem lehet megmenteni, az alkalmazás zökkenőmentesen jár le a munkamenettel — egy tiszta, tízmásodperces „a munkameneted lejárt, kérlek jelentkezz be újra" értesítés —, egy csendes rúgás helyett a bejelentkezési képernyő felé.
Újracsatlakozás: add hozzá a kliensen, majd helyezd át a backendre
A v1.6.9 egy megszakadt leiratozási kapcsolatot helyreállíthatóvá tett a végzetes helyett. Amikor a backend jelezte, hogy a felfelé irányuló beszédkapcsolat elesett, a kliens abbahagyta a végzetes hiba felszínre hozását, és helyette újracsatlakozott — szándékosan nyugodt UX-szel: egyetlen borostyánszínű „Újracsatlakozás…" értesítés (nem kísérletenként egy), egy zöld „Újracsatlakozva" értesítés, amikor visszatért, és a végső „kérlek indítsd újra" hiba csak azután, hogy minden kísérlet kimerült.
Aztán valami, amit szeretek ebben a csokorban: egy kiadással később, a v1.6.10-ben, töröltük azt a kliensoldali újracsatlakozási logikát — nagyjából 113 sort —, és az újracsatlakozást a backendre helyeztük, amely most csendes „újracsatlakozás" státuszüzeneteket bocsát ki, amelyeket a kliens csak visszatükröz. A helyreállítást a kliensen építettük meg, eldöntöttük, hogy a backend a megfelelő tulajdonos, és áthelyeztük. Ez nem ingadozás; ez egy nehéz probléma őszinte életciklusa. A kapcsolat két vége közt szétosztott megbízhatósági logika rossz jel — válassz tulajdonost. (A backend végül ennek a végleges tulajdonosa lett, ami az az újracsatlakozási történet, amelyet a miért áll le az AI jegyzetelőd rossz Wi-Fin mesél el.)
Ugyanez a kiadás emberivé tette a felvételi időkorlátokat: nyers hiba helyett egy figyelmeztető értesítés, amely hagyja, hogy a felvétel folytatódjon, és egy tiszta „Felvételi korlát elérve" üzenet, amikor tényleg leáll — a barátságos mondat felszínre hozva, a belső előtag lecsupaszítva.
Egyetlen újrapróbálkozás, hogy uralja mindet
A v1.6.11-re ugyanaz az újrapróbálkozás-visszalépéssel logika három különböző helyre volt bemásolva — hitelesítés, frissítések, munkamenet-lebontás. Így egyetlen modulba lett összevonva, egyetlen közös szabállyal arról, hogy mit érdemes egyáltalán újrapróbálni: az átmeneti hálózati hibák (kapcsolat-visszaállítás, timeout, DNS-csuklás, socket-megszakadás) exponenciális visszalépéssel újrapróbálódnak; egy valódi szerverhiba gyorsan elbukik, mert egy valódi 4xx újrapróbálása csak a felhasználó idejét pazarolja. Egy osztályozó, néhány elnevezett előbeállítás, három hívó átköltöztetve rá.
Mit tanított ez a csokor
- Egy kezeletlen
erroresemény összeomlás, nem naplósor. A Node újradobja azokat az error eseményeket, amelyeknek nincs hallgatója. Távolítsd el a hallgatókat egy socketen, amelyet talán még lebontasz, és újra kell csatolnod egy no-op hibaelnyelőt — különben egyetlen megszakadt kapcsolat magával viszi az egész alkalmazást. - Frissítsd a hitelesítő adatokat a lejárat előtt és reaktívan egy 401-re — mutexszel mindkettőn. A proaktív bejelentkezve tart; a reaktív elkapja a peremeseteket; a mutex azt jelenti, hogy N egyidejű hívó pontosan egy frissítést okoz, egy újrapróbálkozási felső korlát pedig azt jelenti, hogy egy rossz token nem pöröghet örökké.
- Csak átmeneti hibáknál próbálj újra, és központosítsd az osztályozót. „Érdemes ezt újrapróbálni?" egyetlen döntés, amely egyetlen helyre tartozik. Egy valódi hiba újrapróbálása csak egy lassabb kudarc.
- Döntsd el, ki birtokolja az újracsatlakozást. A kliensen adtuk hozzá, majd a backendre helyeztük. A két végponton élő helyreállítási logika hibagenerátor; adj neki egyetlen otthont.
Ez a megbízhatósági történet harmadik fejezete, amely GeekBye v2-vé válik. Az előző fejezetért lásd töröltünk 5000 sor audiokódot, és a leiratok kétszer kezdtek megjelenni (v1.6.0); azért, hogy az újracsatlakozás végül hol állapodott meg, miért áll le az AI jegyzetelőd rossz Wi-Fin; a teljes ívért pedig a szoftver tökéletességig szállításának anatómiája.