Steven
Steven5 min. skaitymo

Nutrūkęs ryšys neturėtų nulaužti visos programos — bet mūsiškė nulauždavo

Kai mūsų serverio dalis nulūžo pačiame susitikimo įkarštyje, ji ne tik sustabdė transkripciją — ji nulaužė visą programą. Priežastis buvo vienas neapdorotas įvykis, o pataisymas tilpo į dešimt eilučių. Tai leidimų grupė, dėl kurios GeekBye lieka prisijungęs ir susietas per tuos dalykus, kurie anksčiau jį žudydavo.

Inžinerija
Patikimumas
Electron
GeekBye leidimai
Nutrūkęs ryšys neturėtų nulaužti visos programos — bet mūsiškė nulauždavo

Kai kurios klaidos nutraukia funkciją. Blogiausios nuverčia viską aplink ją. Ankstyvame GeekBye leidime, jei mūsų serverio dalis nulūždavo, kol buvai pačiame susitikimo įkarštyje, transkripcija ne tik sustodavo — lūždavo visa programa. Vienas nutrūkęs ryšys, ir visas langas dingdavo.

Leidimų grupė, kuri tai ištaisė — nuo v1.6.8 iki v1.6.11 — yra nedėkingo darbo „lik prisijungęs, lik susietas" meistriškumo pamoka. Pagrindinis pataisymas buvo dešimt eilučių. Štai kas buvo tos dešimt eilučių ir viskas kita, kas išplaukė kartu su jomis.

Dešimt eilučių tarp atsijungimo ir lūžio

Lūžis gyveno garso kode. Kai transkripcijos sesija ardydavo savo WebSocket ryšius, ji juose iškviesdavo removeAllListeners() — protingas sutvarkymas. Bet štai Node.js detalė, kuri sutvarkymą paverčia katastrofa: error įvykis be klausytojo nėra ignoruojamas. Jis iš naujo metamas kaip nepagauta išimtis.

Tad įsivaizduok seką, kai serverio dalis nulūžta: lizdas turi error įvykį, įtrauktą į eilę suveikti. Sutvarkymas paleidžia removeAllListeners(), nuimdamas tvarkyklę, kuri jį būtų pagavusi. Akimirką vėliau į eilę įtraukta klaida suveikia — į tuštumą. Node mato error įvykį, kurio niekas neklauso, ir daro vienintelį dalyką, kurį jo kontraktas leidžia: meta jį kaip nepagautą išimtį, o tai nulaužia procesą. Naudotojo serverio dalis žagtelėjo dvi sekundes, ir jo programa dingo.

Pataisymas (v1.6.8) yra beveik antiklimaksiškas. Iškart nuėmus klausytojus, iš naujo prikabink sąmoningai tuščią error tvarkyklę:

this.micWebSocket.removeAllListeners()
this.micWebSocket.on('error', () => {}) // absorb late error events

Ta tuščia funkcija yra visa esmė. Ji laukiančiai klaidai suteikia vietą, kur nusileisti — surinktuvą — tad Node turi klausytoją ir niekada nemeta iš naujo. Tas pats commitas taip pat sutvarkė pusiau atvirus lizdus, kai bandymas prisijungti nepavyksta pusiaukelėje, kad sugadintas prisijungimas negalėtų palikti gyvo lizdo kabančio su laukiančia klaida. Commito komentaras tai sako tiesiai: „Be šito laukiančios klaidos tampa nepagautomis išimtimis ir nulaužia programą." Dešimt eilučių visišką lūžį pavertė tyliu, atkuriamu atsijungimu.

Pamoka verta daugiau nei pats pataisymas: kaskart, kai iškvieti removeAllListeners() lizde ar sraute, kurį galbūt vis dar ardai, pirmiausia iš naujo prikabink tuščią error surinktuvą. Neapdorotas error įvykis yra lūžis, o ne žurnalo eilutė.

Likti prisijungus: atnaujink iš anksto ir atnaujink atšokus

„Lik susietas" pusė turėjo dvynę — „lik prisijungęs" pusę. Prieš šią grupę GeekBye autentifikacijos tokenas tiesiog baigdavo galioti — po kurio laiko tave be ceremonijų išmesdavo atgal į prisijungimo langą, pačiame darbo įkarštyje, be jokios matomos priežasties.

v1.6.8 tai ištaisė iš abiejų pusių:

  • Proaktyviai: laikmatis atnaujina tokeną prieš jam baigiant galioti, suplanuotas galiojimo pabaigai minus sumanus buferis (tokeno gyvavimo trukmės dalis, apribota iš apačios ir iš viršaus). Esi atnaujintas anksčiau, nei apskritai pastebėtum.
  • Reaktyviai: jei užklausa vis tiek grįžta su 401 — laikrodžio poslinkis, praleistas laikmatis, atvėsęs nešiojamas kompiuteris — klientas atnaujina vieną kartą ir pakartoja pradinę užklausą lygiai vieną kartą. Vienas sargybos žymuo apriboja tą kartojimą, tad atkaklus 401 niekada negali įsisukti į begalinį ciklą.

Subtili, bet kritinė dalis: abu keliai stovi už mutexo. Jei dešimt užklausų atsitrenkia į 401 tą pačią akimirką, jos nepaleidžia dešimties atnaujinimų — visos laukia to vieno skrydyje esančio atnaujinimo ir tada kartoja. O kai atnaujinimo tikrai neįmanoma išgelbėti, programa užbaigia sesiją sklandžiai — aiškus dešimties sekundžių pranešimas „jūsų sesija baigėsi, prisijunkite iš naujo" — vietoj tylaus spyrio į prisijungimo langą.

Pakartotinis prisijungimas: pridėk jį kliente, paskui perkelk į serverio dalį

v1.6.9 padarė nutrūkusį transkripcijos ryšį atkuriamą, o ne lemtingą. Kai serverio dalis pranešdavo, kad aukštupio kalbos ryšys nutrūko, klientas nustodavo rodyti lemtingą klaidą ir vietoj to prisijungdavo iš naujo — su sąmoningai ramiu UX: vienas gintarinis pranešimas „Prisijungiama iš naujo…" (ne po vieną kiekvienam bandymui), žalias pranešimas „Vėl prisijungta", kai jis sugrįždavo, ir galutinė klaida „prašome paleisti iš naujo" tik išnaudojus kiekvieną bandymą.

Tada tai, kas man patinka šioje grupėje: vienu leidimu vėliau, v1.6.10, mes ištrynėme tą kliento pusės pakartotinio prisijungimo logiką — apie 113 eilučių — ir perkėlėme pakartotinį prisijungimą į serverio dalį, kuri dabar siunčia tylius būsenos pranešimus „prisijungiama iš naujo", kuriuos klientas tiesiog atspindi. Atkūrimą pastatėme kliente, nusprendėme, kad teisingas savininkas yra serverio dalis, ir perkėlėme jį. Tai ne dvejonės; tai sąžiningas sunkios problemos gyvavimo ciklas. Patikimumo logika, išskaidyta į abu ryšio galus, yra prasto dizaino ženklas — pasirink savininką. (Serverio dalis galiausiai tapo galutine to savininke, o tai yra pakartotinio prisijungimo istorija, papasakota kodėl jūsų DI užrašininkas sustoja per prastą Wi-Fi.)

Tas pats leidimas įrašymo laiko ribas padarė žmoniškas: vietoj žalios klaidos — įspėjamasis pranešimas, kuris leidžia įrašymui tęstis, ir aiškus pranešimas „Pasiektas įrašymo limitas", kai jis iš tikrųjų sustoja — draugiškas sakinys iškeltas į paviršių, vidinis priešdėlis pašalintas.

Vienas kartojimas, valdantis visus

Iki v1.6.11 ta pati kartojimo su atsitraukimu logika buvo nukopijuota į tris skirtingas vietas — autentifikaciją, atnaujinimus, sesijos ardymą. Tad ji buvo sujungta į vieną modulį su viena bendra taisykle apie tai, ką apskritai verta kartoti: laikini tinklo sutrikimai (ryšio atstata, laiko limitas, DNS žagsulys, lizdo nutrūkimas) kartojami su eksponentiniu atsitraukimu; tikra serverio klaida žlunga greitai, nes tikro 4xx kartojimas tik švaisto naudotojo laiką. Vienas klasifikatorius, keli įvardyti išankstiniai nustatymai, trys iškvietėjai perkelti į jį.

Ko išmokė ši grupė

  1. Neapdorotas error įvykis yra lūžis, o ne žurnalo eilutė. Node iš naujo meta error įvykius, kurie neturi klausytojo. Nuimk klausytojus nuo lizdo, kurį galbūt vis dar ardai, ir privalai iš naujo prikabinti tuščią error surinktuvą — kitaip vienas nutrūkęs ryšys nusineš visą programą.
  2. Atnaujink prisijungimo duomenis iki galiojimo pabaigos ir reaktyviai gavus 401 — su mutexu ant abiejų. Proaktyvus laiko tave prisijungusį; reaktyvus pagauna kraštinius atvejus; mutexas reiškia, kad N vienalaikių iškvietėjų sukelia lygiai vieną atnaujinimą, o kartojimo riba reiškia, kad blogas tokenas negali suktis amžinai.
  3. Kartok tik esant laikinoms klaidoms ir centralizuok klasifikatorių. „Ar verta tai kartoti?" yra vienas sprendimas, priklausantis vienai vietai. Tikros klaidos kartojimas yra tiesiog lėtesnis žlugimas.
  4. Nuspręsk, kas valdo pakartotinį prisijungimą. Pridėjome jį kliente, paskui perkėlėme į serverio dalį. Atkūrimo logika, gyvenanti abiejuose galuose, yra klaidų generatorius; suteik jai vienintelius namus.

Tai trečias patikimumo istorijos, tampančios GeekBye v2, skyrius. Ankstesnį skyrių rasite ištrynėme 5000 garso kodo eilučių, ir transkripcijos pradėjo rodytis dukart (v1.6.0); tai, kur pakartotinis prisijungimas galiausiai nusėdo, kodėl jūsų DI užrašininkas sustoja per prastą Wi-Fi; o visą lanką — programinės įrangos tobulinimo iki tobulybės anatomijoje.