
Bezpečnostná kontrola, ktorá spravila našu aplikáciu nezatvárateľnou
Automatická aktualizácia bola najťažšia funkcia, akú sme kedy vydali — šesť vydaní za štyri dni, aby prestala kaziť aplikáciu. Najhoršiu chybu sme spôsobili sami snahou byť opatrní: 500-milisekundová „bezpečnostná" kontrola, ktorá premenila neúspešnú aktualizáciu na proces, ktorý sa doslova nedal zatvoriť.
Každý vývojár desktopových aplikácií presne raz podcení automatickú aktualizáciu. Vyzerá ako vyriešený problém — knižnica stiahne novú verziu a reštartuje aplikáciu. Potom to vydáš a zistíš, že „reštartuj aplikáciu" je jedna z najnebezpečnejších vecí, o aké možno program požiadať, lebo sa to deje presne vo chvíli, keď aplikácia rozoberá sama seba a má najmenšiu rezervu na chybu.
Automatická aktualizácia GeekBye si vyžiadala šesť vydaní za štyri dni — od v1.5.14 po v1.5.19 — kým sa ustálila. Toto je príbeh najhoršej chyby z toho úseku, ktorú sme si spôsobili sami snahou byť opatrní.
Šesť vydaní a to jedno, na ktorom záležalo
Oblúk sa začal všedne. v1.5.14 opravilo trápnu chybu úrovne preklepu: kanál aktualizácií mieril na názov GitHub repozitára, ktorý neexistoval, takže aktualizátor kontroloval 404. v1.5.15 pridalo manuálne tlačidlo „Check for Updates" a skutočné chybové hlásenie. Potom sa začali chyby quitAndInstall a vydania išli rýchlo — lebo keď je tvoj mechanizmus aktualizácie pokazený, nemôžeš jeho opravu vydať cez mechanizmus aktualizácie. Každá iterácia je stávka na manuálnu preinštaláciu.
To, na ktorom záleží, je v1.5.18. Celý jeho obsah bol jediný commit s názvom, z ktorého sa mi dodnes zošklebí tvár: restore original quitAndInstall behavior to prevent unkillable app.
Ako „opatrnosť" zabila aplikáciu
Tu je nastavenie. Keď je aktualizácia stiahnutá, quitAndInstall z Electronu má aplikáciu zatvoriť a vymeniť ju za novú verziu. V skoršom vydaní sa niekto — celkom rozumne — obával, že zatvárať bezpodmienečne je riskantné. Čo ak inštalácia zlyhá? Nebolo by bezpečnejšie zatvárať len vtedy, keď všetko vyzerá zdravo?
Tak kód obrástol poistkou, ktorá vyzerala rozumne:
autoUpdater.quitAndInstall(false, true)
setTimeout(() => {
if (this.updateDownloaded)
app.quit() // only quit if the update is still "good"
else console.log('Error detected — keeping app open')
}, 500)
Logika: spusti inštaláciu, počkaj pol sekundy a vynúť finálne app.quit() len vtedy, keď je príznak updateDownloaded stále pravdivý — inak nechaj aplikáciu otvorenú, aby používateľ neostal na suchu.
Pasca je o jeden riadok ďalej, v obsluhe chyby. Tá obsluha nastavovala this.updateDownloaded = false. Predstav si teda neúspešnú inštaláciu: udalosť error sa spustí a vymaže príznak. Lenže quitAndInstall už začal rozoberať — zatvoril okná a odstránil poslucháčov ukončenia aplikácie. Potom sa zobudí 500-ms časovač, skontroluje teraz-nepravdivý príznak, rozhodne „zistená chyba, nechaj aplikáciu otvorenú" a preskočí app.quit().
Teraz máš, konkrétne na macOS, najhorší možný stav. macOS nezatvorí aplikáciu len preto, že sa zatvorilo jej posledné okno — to je správanie window-all-closed, na ktoré sa spolieha každá aplikácia na Macu. Takže proces stále žije, ale nemá žiadne okno, žiadnu cestu cez lištu menu a jeho poslucháči ukončenia sú vytrhnutí. Nie je na čo kliknúť. Cmd-Q nemá s čím hovoriť. Jediný východ je Force Quit z Activity Monitor. „Bezpečnostná" kontrola premenila neúspešnú aktualizáciu — naprávateľnú otravu — na zombie, ktoré sa nedalo zabiť.
Oprava: rozobratie musí byť bezpodmienečné
Oprava vo v1.5.18 je takmer agresívne nudná, a to je celý zmysel. Odstraňuje šikovnosť:
- Odstráň poslucháčov
window-all-closedabefore-quit, ktorí by mohli prekážať. - Znič každé okno —
window.destroy(), niewindow.close(). Zatvorenie môže vetovať obsluha; zničenie nie. Keď sa zaväzuješ vypnúť, nepýtaš sa zdvorilo. - Zavolaj
quitAndInstall. - Zavolaj
app.quit()bezpodmienečne.
Žiadny príznak, žiadny časovač, žiadne „nechaj otvorené pre istotu". Lebo pravda o vetve vypnutia je taká, že spolovice dokončené vypnutie je horšie než ktorýkoľvek z výsledkov. Úplne zatvoriť je v poriadku. Úplne ostať otvorený je v poriadku. Jediný stav, ktorý nikdy nesmieš dosiahnuť, je rozobratý, ale stále bežiaci — a to je presne stav, v ktorom ťa podmienené zatvorenie môže uväzniť.
Ďalšie dve lekcie z toho istého týždňa
Nezabíjateľná chyba je titulok, ale šesťvydaňová naháňačka zakalila ešte dva návyky, ktoré stoja za ukradnutie.
Filtruj telemetriu pádov podľa presných signatúr, nie širokých kľúčových slov. Uprostred naháňačky sme zistili, že naše hlásenie chýb bolo nastavené zahodiť všetko, čo obsahovalo slová ako permission, token alebo microphone — pokus zrezať šum, ktorý potichu prehĺtal skutočné pády, ktoré tie slová náhodou spomínali. Vytrhli sme deka-filtre a nahradili ich presnými reťazcami odmietnutia (konkrétnou správou, ktorú macOS vydá pri odmietnutí povolenia) a konkrétnymi prechodnými sieťovými kódmi ako ERR_NETWORK_CHANGED. Znižovanie šumu a skrývanie chýb sú ten istý gombík otočený opačnými smermi; ak filtruješ podľa pocitu, odfiltruješ presne to, čo si potreboval vidieť.
Každá automatická vetva potrebuje manuálny únikový východ. Automatická aktualizácia je zo svojej podstaty najlepšia možná snaha — siete vypadávajú, inštalácie zlyhávajú. Tak každý režim zlyhania dostal ľudskú záložnú možnosť: manuálne tlačidlo „Check for Updates", opakovania s exponenciálnym odstupom, časovač opätovnej kontroly a — vyhradenú konkrétne pre prípad, keď manuálny pokus používateľa zlyhal — správu jednoduchými slovami „vymaž aplikáciu a preinštaluj ju zo stránky". Automatická vetva je pohodlie; manuálna vetva je záruka.
Ponaučenie
- Poistka okolo nezvratnej akcie je nebezpečnejšia než samotná akcia. Podmienené zatvorenie sa pokúšalo zabrániť zlej aktualizácii zatvoriť aplikáciu a namiesto toho vytvorilo stav horší než zatvorenie aj nezatvorenie. Vetvy vypnutia a inštalácie by mali byť bezpodmienečné a idempotentné — nikdy nepodmienené premenlivým príznakom, ktorý ti iná obsluha môže vytiahnuť spod nôh.
- Na macOS „žiadne okná" nie je „žiadna aplikácia". Akákoľvek logika rozoberania musí počítať s platformou, kde proces bez okien beží ďalej. Testuj vetvu zlyhania na skutočnom OS, nie len šťastnú vetvu.
- Funkciu, ktorú vydávaš cez systém aktualizácie, nemožno otestovať cez systém aktualizácie. Práve táto asymetria je dôvod, prečo si automatická aktualizácia zaslúži paranoidný, bezpodmienečný, dôkladne manuálne overený kód. Ľahký spôsob opravy dostaneš až potom, ako to už funguje.
Toto je najskoršia kapitola práce na spoľahlivosti, ktorá sa napokon stala GeekBye v2. O tom, kam tá cesta viedla, si prečítaj čo v skutočnosti stojí verzia 2 (v2.0.0) a celý oblúk v anatómii dodávania softvéru do dokonalosti.