Steven
Steven5 min lukuaika

Katkennut yhteys ei saisi kaataa koko sovellusta — mutta meidän kaatoi

Kun taustapalvelimemme katkesi kesken kokouksen, se ei vain keskeyttänyt litterointia — se kaatoi koko sovelluksen. Syynä oli yksi käsittelemätön tapahtuma, ja korjaus oli kymmenen riviä. Tämä on se julkaisurypäs, joka sai GeekByen pysymään kirjautuneena ja yhteydessä niiden asioiden läpi, jotka ennen tappoivat sen.

Ohjelmistokehitys
Luotettavuus
Electron
GeekBye-julkaisut
Katkennut yhteys ei saisi kaataa koko sovellusta — mutta meidän kaatoi

Jotkin bugit keskeyttävät yhden toiminnon. Pahimmat vievät mukanaan kaiken ympäriltään. Eräässä varhaisessa GeekBye-koonnoksessa, jos taustapalvelimemme katkesi kesken kokouksen, litterointi ei vain pysähtynyt — koko sovellus kaatui. Yksi katkennut yhteys, ja koko ikkuna oli poissa.

Julkaisurypäs, joka korjasi tämän — v1.6.8:sta v1.6.11:een — on mestarinäyte epäglamouri­sesta työstä nimeltä "pysy kirjautuneena, pysy yhteydessä". Pääkorjaus oli kymmenen riviä. Tässä on, mitä ne kymmenen riviä olivat, ja kaikki muu, mikä julkaistiin niiden rinnalla.

Kymmenen riviä katkoksen ja kaatumisen välissä

Kaatuminen asui äänikoodissa. Kun litterointi-istunto purki WebSocket-yhteytensä, se kutsui niille removeAllListeners() — järkevää siivousta. Mutta tässä on se Node.js-yksityiskohta, joka muuttaa siivouksen katastrofiksi: kuuntelijatonta error-tapahtumaa ei jätetä huomiotta. Se heitetään uudelleen käsittelemättömänä poikkeuksena.

Kuvittele siis tapahtumaketju, kun taustapalvelin katkeaa: socketilla on error-tapahtuma jonossa laukeamassa. Siivous ajaa removeAllListeners(), poistaen käsittelijän, joka olisi napannut sen. Hetkeä myöhemmin jonossa oleva virhe laukeaa — tyhjyyteen. Node näkee error-tapahtuman, jota kukaan ei kuuntele, ja tekee ainoan asian, jonka sen sopimus sallii: se heittää sen käsittelemättömänä poikkeuksena, joka kaataa prosessin. Käyttäjän taustapalvelin nikotteli kaksi sekuntia ja hänen sovelluksensa katosi.

Korjaus (v1.6.8) on lähes antikliimaksi. Heti kuuntelijoiden poiston jälkeen liitä takaisin tarkoituksellisesti tyhjä error-käsittelijä:

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

Tuo tyhjä funktio on koko pointti. Se antaa odottavalle virheelle paikan, johon laskeutua — nielun — jotta Nodella on kuuntelija eikä se koskaan heitä mitään uudelleen. Sama commit siivosi myös puoliksi avoimet socketit, kun yhteydenotto epäonnistuu kesken, jottei epäonnistunut yhteydenotto voi jättää elävää socketia roikkumaan virheen kanssa, joka odottaa laukeamistaan. Commitin kommentti sanoo sen suoraan: "Ilman tätä odottavista virheistä tulee käsittelemättömiä poikkeuksia ja ne kaatavat sovelluksen." Kymmenen riviä muutti täyden kaatumisen hiljaiseksi, palautuvaksi katkokseksi.

Opetus on arvokkaampi kuin itse korjaus: aina kun kutsut removeAllListeners() socketilla tai virralla, jota saatat vielä purkaa, liitä ensin takaisin tyhjä error-nielu. Käsittelemätön error-tapahtuma on kaatuminen, ei lokirivi.

Kirjautuneena pysyminen: uusi etukäteen ja uusi kimmokkeesta

"Pysy yhteydessä" -puoliskolla oli "pysy kirjautuneena" -kaksonen. Ennen tätä ryvästä GeekByen todennustoken yksinkertaisesti vanheni — hetken kuluttua sinut heitettiin seremoniatta takaisin kirjautumisruutuun, kesken työnkulun, ilman näkyvää syytä.

v1.6.8 korjasi tämän molemmista suunnista:

  • Ennakoiva: ajastin uusii tokenin ennen sen vanhenemista, ajoitettuna vanhenemishetkeen miinus fiksu puskuri (murto-osa tokenin eliniästä, ala- ja ylärajattuna). Sinut on uusittu, ennen kuin edes huomaisit.
  • Reaktiivinen: jos pyyntö silti palaa 401:llä — kellon poikkeama, ohi mennyt ajastin, kylmä läppäri — asiakas uusii kerran ja yrittää alkuperäistä pyyntöä uudelleen täsmälleen kerran. Yksi vartiolippu rajaa tämän uudelleenyrityksen, jottei jatkuva 401 voi koskaan kiertyä äärettömäksi silmukaksi.

Hienovarainen mutta ratkaiseva osa: molemmat polut ovat mutexin takana. Jos kymmenen pyyntöä osuu 401:een samalla hetkellä, ne eivät laukaise kymmentä uusintaa — ne kaikki odottavat yhtä käynnissä olevaa uusintaa ja yrittävät sitten uudelleen. Ja kun uusintaa ei aidosti voida pelastaa, sovellus vanhentaa istunnon hallitusti — selkeä kymmenen sekunnin ilmoitus "istuntosi vanheni, kirjaudu uudelleen sisään" — sen sijaan että sinut heitettäisiin hiljaa kirjautumisruutuun.

Uudelleenyhdistäminen: lisää se asiakkaaseen, siirrä se sitten taustapalvelimelle

v1.6.9 teki katkenneesta litterointiyhteydestä palautuvan kohtalokkaan sijaan. Kun taustapalvelin raportoi, että ylävirran puheyhteys oli katkennut, asiakas lakkasi nostamasta esiin kohtalokasta virhettä ja yhdisti sen sijaan uudelleen — tarkoituksellisen rauhallisella käyttökokemuksella: yksi keltainen "Yhdistetään uudelleen…" -ilmoitus (ei yhtä per yritys), vihreä "Yhteys palautettu" -ilmoitus, kun se palasi, ja lopullinen "käynnistä uudelleen" -virhe vasta sen jälkeen, kun jokainen yritys oli käytetty.

Ja sitten jotain, mistä pidän tässä ryväksessä: yhtä julkaisua myöhemmin, versiossa v1.6.10, poistimme tuon asiakaspuolen uudelleenyhdistämislogiikan — noin 113 riviä — ja siirsimme uudelleenyhdistämisen taustapalvelimelle, joka nyt lähettää hiljaisia "yhdistetään uudelleen" -tilaviestejä, jotka asiakas vain heijastaa. Rakensimme palautumisen asiakkaaseen, päätimme että taustapalvelin oli oikea omistaja, ja siirsimme sen. Se ei ole jahkailua; se on vaikean ongelman rehellinen elinkaari. Yhteyden molempien päiden kesken jaettu luotettavuuslogiikka on paha merkki — valitse omistaja. (Taustapalvelimesta tuli lopulta tämän lopullinen omistaja, ja se on uudelleenyhdistämisen tarina, joka kerrotaan artikkelissa miksi AI-muistiinpanijasi pysähtyy huonolla Wi-Fillä.)

Sama julkaisu teki nauhoitusajan rajoista inhimillisiä: raa'an virheen sijaan varoitusilmoitus, joka antaa nauhoituksen jatkua, ja selkeä "Nauhoitusraja saavutettu" -viesti, kun se todella pysähtyy — ystävällinen lause nostettu esiin, sisäinen etuliite riisuttu pois.

Yksi uudelleenyritys hallitsemaan niitä kaikkia

Versioon v1.6.11 mennessä sama uudelleenyritys-perääntymislogiikka oli kopioitu kolmeen eri paikkaan — todennus, päivitykset, istunnon purku. Niinpä se yhdistettiin yhdeksi moduuliksi, jossa on yksi jaettu sääntö siitä, mitä ylipäätään kannattaa yrittää uudelleen: ohimenevät verkkovirheet (yhteyden nollaus, aikakatkaisu, DNS-nikottelu, socketin katkeaminen) yritetään uudelleen eksponentiaalisella perääntymisellä; aito palvelinvirhe kaatuu heti, koska aidon 4xx:n uudelleenyrittäminen vain tuhlaa käyttäjän aikaa. Yksi luokittelija, muutama nimetty esiasetus, kolme kutsujaa siirretty sen päälle.

Mitä tämä rypäs opetti

  1. Käsittelemätön error-tapahtuma on kaatuminen, ei lokirivi. Node heittää uudelleen error-tapahtumat, joilla ei ole kuuntelijaa. Poista kuuntelijat socketilta, jota saatat vielä purkaa, ja sinun on liitettävä takaisin tyhjä error-nielu — muuten yksi katkennut yhteys vie koko sovelluksen mukanaan.
  2. Uusi tunnukset ennen vanhenemista ja reaktiivisesti 401:n kohdalla — molemmissa mutex. Ennakoiva pitää sinut kirjautuneena; reaktiivinen nappaa reunatapaukset; mutex tarkoittaa, että N samanaikaista kutsujaa aiheuttaa täsmälleen yhden uusinnan, ja uudelleenyrityskatto tarkoittaa, ettei huono token voi kiertää ikuisesti.
  3. Yritä uudelleen vain ohimenevissä virheissä ja keskitä luokittelija. "Kannattaako tätä yrittää uudelleen?" on yksi päätös, joka kuuluu yhteen paikkaan. Aidon virheen uudelleenyrittäminen on vain hitaampi epäonnistuminen.
  4. Päätä, kenelle uudelleenyhdistäminen kuuluu. Lisäsimme sen asiakkaaseen, sitten siirsimme sen taustapalvelimelle. Molemmissa päissä asuva palautumislogiikka on buginlähde; anna sille yksi koti.

Tämä on kolmas luku luotettavuustarinassa, josta tulee GeekBye v2. Edellisen luvun osalta katso poistimme 5 000 riviä äänikoodia ja litteroinnit alkoivat näkyä kahdesti (v1.6.0); siitä, mihin uudelleenyhdistäminen lopulta asettui, miksi AI-muistiinpanijasi pysähtyy huonolla Wi-Fillä; ja koko kaaresta anatomia ohjelmiston toimittamisesta täydellisyyteen.