Steven
Steven6 min čítania

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.

Inžinierstvo
Spoľahlivosť
Electron
Vydania GeekBye
Prerušené spojenie by nemalo zhodiť celú aplikáciu — no tá naša ju zhodila

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" 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

  1. Neobslúžená udalosť error je 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.
  2. 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.
  3. 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.
  4. 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.