Steven
Steven6 perc olvasás

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.

Mérnöki munka
Megbízhatóság
Electron
GeekBye kiadások
Egy megszakadt kapcsolatnak nem szabadna összeomlasztania az egész alkalmazást — de a miénk ezt tette

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

  1. Egy kezeletlen error esemé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.
  2. 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é.
  3. 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.
  4. 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.

Kapcsolódó cikkek

A biztonsági ellenőrzés, amitől lehetetlen lett kilépni az appból
Steven
Steven6 perc olvasás

A biztonsági ellenőrzés, amitől lehetetlen lett kilépni az appból

Az automatikus frissítés volt a legnehezebb funkció, amit valaha kiadtunk — négy nap alatt hat kiadás kellett, hogy megakadályozzuk az app tönkretételét. A legrosszabb hibát mi magunk okoztuk, miközben óvatosak próbáltunk lenni: egy 500 ezredmásodperces „biztonsági" ellenőrzés egy sikertelen frissítést olyan folyamattá változtatott, amelyből szó szerint nem lehetett kilépni.

Mérnöki munka
Electron
Megbízhatóság
A tökéletesre csiszolt szoftverszállítás anatómiája: hogyan kapta el a kódellenőrzés azt, amit a tesztek nem
Steven
Steven6 perc olvasás

A tökéletesre csiszolt szoftverszállítás anatómiája: hogyan kapta el a kódellenőrzés azt, amit a tesztek nem

A GeekBye v2 sorozatban újra és újra ugyanaz történik: egy javítás átmegy minden teszten a fejlesztő gépén, majd a kódellenőrzés bebizonyítja, hogy szinte mindenki másnál elbukott volna. Ez a kilenc kiadás mögötti munkafolyamat — az ellenőrzési kapu, a javítás-előbb elkapott hibák és a szállítás-előtti tesztelési fegyelem, amely a „nálam működik” állapotot „működik” állapottá változtatja.

Mérnöki munka
Folyamat
Kódellenőrzés
A Mac-alkalmazásod megkapja a mikrofon-hozzáférést — aztán minden indításkor elfelejti
Steven
Steven5 perc olvasás

A Mac-alkalmazásod megkapja a mikrofon-hozzáférést — aztán minden indításkor elfelejti

A GeekBye mikrofonengedélyt kért, megadtad, és működött. A következő indításnál: eltűnt. És az alkalmazás egyáltalán meg sem jelent a Rendszerbeállítások → Mikrofon alatt. A bűnös egy macOS biztonsági funkció volt, amely csendben egy eltűnő elérési útról futtatta az alkalmazást — íme a diagnózis és az egyetlen kérdéssel megoldó javítás.

macOS
Engedélyek
Mérnöki munka