
Bezpečnostní kontrola, kvůli které nešla naše aplikace ukončit
Automatická aktualizace byla nejtěžší funkce, jakou jsme kdy vydali — šest vydání za čtyři dny, aby přestala odrovnávat aplikaci. Nejhorší chybu jsme si způsobili sami ve snaze být opatrní: 500milisekundová „bezpečnostní" kontrola, která z neúspěšné aktualizace udělala proces, který se doslova nedal ukončit.
Každý vývojář desktopové aplikace podcení automatickou aktualizaci právě jednou. Vypadá to jako vyřešený problém — knihovna stáhne novou verzi a restartuje vaši aplikaci. Pak to vydáte a zjistíte, že „restartovat aplikaci" je jedna z nejnebezpečnějších věcí, o kterou lze program požádat, protože se to děje přesně ve chvíli, kdy se aplikace sama boří a má nejmenší prostor pro chybu.
Stabilizace automatické aktualizace v GeekBye zabrala šest vydání za čtyři dny — v1.5.14 až v1.5.19. Tohle je příběh nejhorší chyby v tomto úseku, kterou jsme si způsobili sami ve snaze být opatrní.
Šest vydání a to jediné, na kterém záleželo
Oblouk začal všedně. v1.5.14 opravila trapnou chybu třídy překlep: aktualizační kanál ukazoval na název GitHub repozitáře, který neexistoval, takže updater kontroloval 404. v1.5.15 přidala ruční tlačítko „Check for Updates" a skutečnou chybovou hlášku. Pak začaly chyby quitAndInstall a vydání přicházela rychle — protože když je váš aktualizační mechanismus rozbitý, nemůžete opravu poslat skrz aktualizační mechanismus. Každá iterace je sázka na ruční přeinstalaci.
Tím, na kterém záleží, je v1.5.18. Celý jeho obsah byl jediný commit s titulkem, ze kterého se mi pořád svírá žaludek: restore original quitAndInstall behavior to prevent unkillable app.
Jak „opatrnost" odrovnala aplikaci
Tady je zápletka. Když je aktualizace stažená, Electroní quitAndInstall má aplikaci zavřít a vyměnit ji za novou verzi. V dřívějším vydání někdo — rozumně — dostal obavu, že ukončit aplikaci bezpodmínečně je riskantní. Co když instalace skončí chybou? Nebylo by bezpečnější ukončit ji jen tehdy, když všechno vypadá v pořádku?
Tak kód narostl o pojistku, která vypadala rozumně:
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: spustit instalaci, počkat půl sekundy a vynutit závěrečné app.quit() jen tehdy, pokud je příznak updateDownloaded stále true — jinak nechat aplikaci otevřenou, aby uživatel neuvízl.
Past je vzdálená jediný řádek, v obsluze chyby. Ta obsluha nastavovala this.updateDownloaded = false. Představte si tedy neúspěšnou instalaci: událost error se spustí a vymaže příznak. Jenže quitAndInstall už začal demontáž — zavřel okna a odebral posluchače ukončení aplikace. Pak se probudí 500ms časovač, zkontroluje nyní nepravdivý příznak, rozhodne se „error detected, keep the app open" a přeskočí app.quit().
A teď máte, konkrétně na macOS, nejhorší možný stav. macOS neukončí aplikaci jen proto, že se zavřelo její poslední okno — to je chování window-all-closed, na které spoléhá každá aplikace pro Mac. Takže proces je pořád naživu, ale nemá žádné okno, žádnou cestu přes lištu menu a jeho posluchače ukončení byly vytrhány. Není na co kliknout. Cmd-Q nemá s čím mluvit. Jediná cesta ven je Force Quit z Activity Monitor. „Bezpečnostní" kontrola přeměnila neúspěšnou aktualizaci — obtěžující, ale zotavitelnou — na zombie, kterou nešlo zabít.
Oprava: demontáž musí být bezpodmínečná
Náprava ve v1.5.18 je téměř agresivně nudná, což je celý smysl. Odstraňuje tu chytristiku:
- Odstranit posluchače
window-all-closedabefore-quit, kteří by mohli zasahovat. - Zničit každé okno —
window.destroy(), newindow.close(). Zavření může obsluha vetovat, zničení ne. Když se rozhodujete pro vypnutí, neptáte se zdvořile. - Zavolat
quitAndInstall. - Zavolat
app.quit()bezpodmínečně.
Žádný příznak, žádný časovač, žádné „nechme to otevřené pro jistotu". Protože pravda o vypínací cestě je taková, že napůl dokončené vypnutí je horší než kterýkoli z obou výsledků. Úplně ukončit je v pořádku. Úplně zůstat otevřený je v pořádku. Jediný stav, do kterého se nikdy nesmíte dostat, je zbouraný, ale stále běžící — a přesně do toho stavu vás podmíněné ukončení dokáže zaseknout.
Dvě další lekce, které nás naučil stejný týden
Neukončitelná chyba je hlavní titulek, ale ten spěch šesti vydání zocelil dva další návyky, které stojí za ukradení.
Filtrujte telemetrii pádů podle přesných signatur, ne širokých klíčových slov. Uprostřed shonu jsme zjistili, že naše hlášení chyb bylo nastavené tak, aby zahazovalo cokoli, co obsahovalo slova jako permission, token nebo microphone — pokus omezit šum, který ale tiše polykal skutečné pády, jež ta slova náhodou zmiňovaly. Plošné filtry jsme vytrhli a nahradili je přesnými řetězci zamítnutí (konkrétní hláškou, kterou macOS vydá při odepření oprávnění) a konkrétními přechodnými síťovými kódy jako ERR_NETWORK_CHANGED. Redukce šumu a skrývání chyb jsou tentýž knoflík otočený opačným směrem; když filtrujete podle dojmu, vyfiltrujete přesně to, co jste potřebovali vidět.
Každá automatická cesta potřebuje ruční záchrannou páku. Automatická aktualizace je ze své podstaty snaha o co nejlepší výsledek — sítě selhávají, instalace se nezdaří. Takže každý způsob selhání dostal lidskou záložní variantu: ruční tlačítko „Check for Updates", opakování s exponenciálním odstupem, časovač pro opětovnou kontrolu a — vyhrazenou konkrétně pro případ, kdy selhal ruční pokus uživatele — srozumitelnou hlášku „smažte aplikaci a přeinstalujte ji z webu". Automatická cesta je pohodlí; ruční cesta je záruka.
Ponaučení
- Pojistka kolem nevratné akce je nebezpečnější než ta akce samotná. Podmíněné ukončení se snažilo zabránit tomu, aby špatná aktualizace ukončila aplikaci, a místo toho vytvořilo stav horší, než je ukončení i neukončení. Cesty vypnutí a instalace by měly být bezpodmínečné a idempotentní — nikdy podmíněné mutovatelným příznakem, který vám jiná obsluha může podtrhnout pod nohama.
- Na macOS „žádná okna" neznamená „žádná aplikace". Jakákoli logika demontáže musí počítat s platformou, kde proces bez oken běží dál. Otestujte cestu selhání, na skutečném OS, ne jen šťastnou cestu.
- Funkci, kterou vydáváte přes aktualizační systém, nelze přes aktualizační systém otestovat. Právě tato asymetrie je důvod, proč si automatická aktualizace zaslouží paranoidní, bezpodmínečný a důkladně ručně ověřený kód. Opravit ji tou snadnou cestou dostanete šanci až potom, co už funguje.
Tohle je nejranější kapitola práce na spolehlivosti, ze které se nakonec stal GeekBye v2. Kam ta cesta vedla, se dočtete v what a version 2 actually takes (v2.0.0) a celý oblouk v the anatomy of shipping software to perfection.