
Prerušené spojenie by nemalo zhodiť celú aplikáciu — no tá naša ju zhodila
Keď náš backend uprostred stretnutia vypadol, nielenže pozastavil prepis — zhodil celú aplikáciu. Príčinou bola jediná neobslúžená udalosť a oprava mala desať riadkov. Toto je klaster vydaní, vďaka ktorému GeekBye zostáva prihlásený a pripojený aj cez veci, ktoré ho kedysi zabíjali.
Niektoré chyby prerušia funkciu. Tie najhoršie zvalia všetko okolo nej. V ranom builde GeekBye, ak náš backend vypadol, kým ste boli uprostred stretnutia, prepis sa nielen zastavil — spadla celá aplikácia. Jedno prerušené spojenie a celé okno bolo preč.
Klaster vydaní, ktorý to opravil — od v1.6.8 po v1.6.11 — je majstrovská ukážka nevzhľadnej roboty v duchu „zostaň prihlásený, zostaň pripojený". Hlavná oprava mala desať riadkov. Tu je, čím tých desať riadkov bolo, a všetko ostatné, čo prišlo spolu s nimi.
Desať riadkov medzi odpojením a pádom
Pád žil v audiokóde. Keď relácia prepisu búrala svoje WebSockety, volala na nich removeAllListeners() — rozumné upratovanie. Ale tu je detail Node.js, ktorý mení upratovanie na katastrofu: udalosť error bez poslucháča sa neignoruje. Znovu sa vyhodí ako nezachytená výnimka.
Predstavte si teda postupnosť, keď backend vypadne: soket má udalosť error zaradenú v rade na spustenie. Upratovanie spustí removeAllListeners(), čím odoberie obslužný kód, ktorý by ju zachytil. O chvíľu neskôr zaradená chyba vystrelí — do prázdna. Node vidí udalosť error, ktorú nikto nepočúva, a urobí jediné, čo jeho kontrakt dovoľuje: vyhodí ju ako nezachytenú výnimku, čo zhodí proces. Backend používateľa čkol na dve sekundy a jeho aplikácia zmizla.
Oprava (v1.6.8) je takmer antiklimaktická. Hneď po odobraní poslucháčov znovu pripojte zámerne prázdny obslužný kód error:
this.micWebSocket.removeAllListeners()
this.micWebSocket.on('error', () => {}) // absorb late error events
Tá prázdna funkcia je celý zmysel. Dáva čakajúcej chybe miesto, kam pristáť — záchytávač — takže Node má poslucháča a nikdy znovu nevyhodí. Ten istý commit tiež upratal polootvorené sokety, keď pokus o pripojenie zlyhá na polceste, aby zbabraný pokus o pripojenie nemohol nechať živý soket visieť s čakajúcou chybou. Komentár commitu to hovorí naplno: „Bez tohto sa čakajúce chyby stanú nezachytenými výnimkami a zhodia aplikáciu." Desať riadkov premenilo úplný pád na tiché, zotaviteľné odpojenie.
Ponaučenie má väčšiu hodnotu než oprava: vždy, keď voláte removeAllListeners() na sokete alebo prúde, ktorý možno ešte búrate, najprv znovu pripojte prázdny záchytávač error. Neobslúžená udalosť error je pád, nie riadok v logu.
Zostať prihlásený: obnov vopred a obnov pri odraze
Polovica „zostaň pripojený" mala dvojča — polovicu „zostaň prihlásený". Pred týmto klastrom autentifikačný token GeekBye jednoducho vypršal — po čase vás bez okolkov vyhodilo späť na prihlasovaciu obrazovku, uprostred práce, bez viditeľného dôvodu.
v1.6.8 to opravil z oboch strán:
- Proaktívne: časovač obnoví token predtým, než vyprší, naplánovaný na moment vypršania mínus šikovná rezerva (zlomok životnosti tokenu, orezaný zdola aj zhora). Ste obnovení skôr, než by ste to vôbec zbadali.
- Reaktívne: ak sa požiadavka aj tak vráti s
401— posun hodín, zmeškaný časovač, vychladnutý laptop — klient obnoví raz a zopakuje pôvodnú požiadavku presne raz. Jediný strážny príznak obmedzí toto opakovanie, takže tvrdohlavý 401 sa nikdy nemôže roztočiť do nekonečnej slučky.
Jemná, no kľúčová časť: obe cesty stoja za mutexom. Ak desať požiadaviek narazí na 401 v tom istom okamihu, nespustia desať obnovení — všetky čakajú na to jedno obnovenie v lete a potom zopakujú. A keď obnovenie naozaj nemožno zachrániť, aplikácia ukončí reláciu plynulo — jasná desaťsekundová hláška „vaša relácia vypršala, prihláste sa znova" — namiesto tichého kopanca na prihlasovaciu obrazovku.
Opätovné pripojenie: pridaj ho na klientovi, potom presuň na backend
v1.6.9 urobil prerušené spojenie prepisu zotaviteľným namiesto fatálneho. Keď backend nahlásil, že nadradené rečové spojenie vypadlo, klient prestal zobrazovať fatálnu chybu a namiesto toho sa znovu pripojil — so zámerne pokojným UX: jedna jantárová hláška „Opätovné pripájanie…" (nie jedna na pokus), zelená hláška „Znovu pripojené", keď sa vrátilo, a terminálna chyba „reštartujte, prosím" až po vyčerpaní každého pokusu.
Potom to, čo mám na tomto klastri rád: o vydanie neskôr, vo v1.6.10, sme túto logiku opätovného pripojenia na strane klienta zmazali — asi 113 riadkov — a opätovné pripojenie sme presunuli na backend, ktorý teraz vysiela tiché stavové správy „opätovné pripájanie", ktoré klient len odzrkadľuje. Zotavenie sme postavili na klientovi, rozhodli sme, že správnym vlastníkom je backend, a presunuli sme ho. To nie je váhanie; to je poctivý životný cyklus ťažkého problému. Logika spoľahlivosti rozdelená na oba konce spojenia je zápach — vyberte vlastníka. (Backend sa nakoniec stal definitívnym vlastníkom tohto, čo je príbeh opätovného pripojenia rozprávaný v prečo sa váš AI zapisovač zastaví pri zlom Wi-Fi.)
To isté vydanie urobilo limity času nahrávania ľudskými: namiesto surovej chyby varovná hláška, ktorá nechá nahrávanie pokračovať, a jasná správa „Dosiahnutý limit nahrávania", keď sa naozaj zastaví — priateľská veta vynesená navrch, interný prefix odstránený.
Jedno opakovanie, čo vládne všetkým
Do v1.6.11 bola tá istá logika opakovania s backoffom skopírovaná na tri rôzne miesta — autentifikácia, aktualizácie, búranie relácie. Tak sa konsolidovala do jediného modulu s jedným spoločným pravidlom o tom, čo sa vôbec oplatí opakovať: prechodné sieťové zlyhania (reset spojenia, timeout, čkanie DNS, zavesenie soketu) sa opakujú s exponenciálnym backoffom; skutočná chyba servera zlyhá rýchlo, pretože opakovanie ozajstného 4xx len plytvá časom používateľa. Jeden klasifikátor, pár pomenovaných presetov, traja volajúci prenesení naň.
Čo tento klaster naučil
- Neobslúžená udalosť
errorje pád, nie riadok v logu. Node znovu vyhadzuje udalosti error, ktoré nemajú poslucháča. Odoberte poslucháčov zo soketu, ktorý možno ešte búrate, a musíte znovu pripojiť prázdny záchytávač error — inak jedno prerušené spojenie vezme so sebou celú aplikáciu. - Obnov prihlasovacie údaje pred vypršaním aj reaktívne pri 401 — s mutexom na oboch. Proaktívne vás drží prihláseného; reaktívne chytí okrajové prípady; mutex znamená, že N súčasných volajúcich spôsobí presne jedno obnovenie, a limit opakovaní znamená, že zlý token sa nemôže zacykliť navždy.
- Opakuj len pri prechodných chybách a centralizuj klasifikátor. „Oplatí sa to opakovať?" je jedno rozhodnutie, ktoré patrí na jedno miesto. Opakovanie skutočnej chyby je len pomalšie zlyhanie.
- Rozhodni, kto vlastní opätovné pripojenie. Pridali sme ho na klientovi, potom sme ho presunuli na backend. Logika zotavenia žijúca na oboch koncoch je generátor chýb; dajte jej jediný domov.
Toto je tretia kapitola príbehu o spoľahlivosti, ktorý sa stáva GeekBye v2. Predošlú kapitolu nájdete v zmazali sme 5000 riadkov audiokódu a prepisy sa začali objavovať dvakrát (v1.6.0); to, kde sa opätovné pripojenie nakoniec usadilo, v prečo sa váš AI zapisovač zastaví pri zlom Wi-Fi; a celý oblúk v anatómii dodávania softvéru k dokonalosti.