Steven
Steven6 мин четене

Проверката за безопасност, която направи приложението ни невъзможно за спиране

Автоматичното обновяване беше най-трудната функция, която някога сме пускали — шест издания за четири дни, за да спрем то да убива приложението. Най-лошата грешка беше тази, която сами въведохме, опитвайки се да сме внимателни: 500-милисекундна "предпазна" проверка, която превърна неуспешно обновяване в процес, който буквално не можеше да бъде спрян.

Инженерство
Electron
Надеждност
Издания на GeekBye
Проверката за безопасност, която направи приложението ни невъзможно за спиране

Всеки разработчик на настолни приложения подценява автоматичното обновяване точно веднъж. Изглежда като решен проблем — библиотека изтегля нова версия и рестартира приложението ти. После го пускаш и научаваш, че "рестартирай приложението си" е едно от най-опасните неща, за които една програма може да бъде помолена, защото се случва в точния момент, в който приложението ти се разрушава и има най-малко резерв за грешки.

Автоматичното обновяване на GeekBye отне шест издания за четири дни — от v1.5.14 до v1.5.19 — за да се стабилизира. Това е историята на най-лошата грешка в този период, която сами причинихме, опитвайки се да сме внимателни.

Шест издания и това, което имаше значение

Всичко започна обикновено. v1.5.14 поправи неудобна грешка от класа на печатните: емисията за обновяване сочеше към име на GitHub хранилище, което не съществуваше, така че обновителят проверяваше 404. v1.5.15 добави ръчен бутон "Check for Updates" и истинско съобщение за грешка. После започнаха грешките с quitAndInstall и изданията заваляха бързо — защото когато механизмът ти за обновяване е счупен, не можеш да пуснеш поправката за него чрез механизма за обновяване. Всяка итерация е залог за ръчна преинсталация.

Това, което има значение, е v1.5.18. Цялото ѝ съдържание беше един-единствен комит със заглавие, което все още ме кара да трепна: restore original quitAndInstall behavior to prevent unkillable app.

Как "внимателността" направи приложението неизползваемо

Ето постановката. Когато обновяване бъде изтеглено, quitAndInstall на Electron би трябвало да затвори приложението и да го смени с новата версия. В по-ранно издание някой — резонно — се притесни, че спирането безусловно е рисковано. Ами ако инсталацията даде грешка? Нямаше ли да е по-безопасно да се спре само ако всичко изглежда наред?

Затова кодът се сдоби със защита, която изглеждаше разумна:

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)

Логиката: задей инсталацията, изчакай половин секунда и наложи окончателното app.quit() само ако флагът updateDownloaded все още е истина — иначе остави приложението отворено, за да не остане потребителят блокиран.

Капанът е на един ред разстояние, в обработчика на грешки. Този обработчик задаваше this.updateDownloaded = false. Затова си представи неуспешна инсталация: събитието error се задейства и изчиства флага. Но quitAndInstall вече беше започнал разрушаването — беше затворил прозорците и премахнал слушателите за спиране на приложението. После 500ms таймерът се събужда, проверява вече-неистинния флаг, решава "открита е грешка, остави приложението отворено" и пропуска app.quit().

Сега имаш, конкретно на macOS, най-лошото възможно състояние. macOS не спира приложение само защото последният му прозорец се е затворил — това е поведението window-all-closed, на което разчита всяко Mac приложение. Така че процесът е все още жив, но има никакъв прозорец, никакъв път през лентата с менюта и премахнати слушатели за спиране. Няма върху какво да се щракне. Cmd-Q няма с какво да говори. Единственият изход е Force Quit от Activity Monitor. "Предпазната" проверка беше превърнала неуспешно обновяване — досадна, но поправима ситуация — в зомби, което не можеш да убиеш.

Поправката: разрушаването трябва да е безусловно

Корекцията във v1.5.18 е почти агресивно скучна, което е и целта. Тя премахва мъдруването:

  1. Премахни слушателите window-all-closed и before-quit, които биха могли да пречат.
  2. Унищожи всеки прозорец — window.destroy(), не window.close(). Close може да бъде отхвърлено от обработчик; destroy не може. Когато си се ангажирал да спираш, не питаш учтиво.
  3. Извикай quitAndInstall.
  4. Извикай app.quit() безусловно.

Никакъв флаг, никакъв таймер, никакво "остави го отворено за всеки случай." Защото истината за пътя на спиране е, че наполовина завършено спиране е по-лошо от която и да е от двете крайности. Пълното спиране е наред. Пълното оставане отворено е наред. Единственото състояние, до което никога не бива да достигаш, е разрушено, но все още работещо — и точно това е състоянието, в което условното спиране може да те остави блокиран.

Още два урока, на които същата седмица научи

Невъзможната за спиране грешка е водеща, но паническото препускане през шест издания затвърди два други навика, които си струва да откраднеш.

Филтрирай телеметрията си за сривове по точни сигнатури, а не по широки ключови думи. По средата на препускането открихме, че отчитането ни на грешки беше конфигурирано да отхвърля всичко, съдържащо думи като permission, token или microphone — опит да се реже шум, който тихо поглъщаше реални сривове, случайно споменаващи тези думи. Изтръгнахме одеалните филтри и ги заменихме с точни низове за отказ (конкретното съобщение, което macOS издава при отказано разрешение) и специфични временни мрежови кодове като ERR_NETWORK_CHANGED. Намаляването на шума и скриването на грешки са едно и също копче, завъртяно в противоположни посоки; ако филтрираш по усещане, ще филтрираш точно нещото, което е трябвало да видиш.

Всеки автоматичен път се нуждае от ръчен авариен изход. Автоматичното обновяване по природа е "най-добро усилие" — мрежите се дъним, инсталациите се провалят. Затова всеки режим на провал получи човешки резервен вариант: ръчният бутон "Check for Updates", повторни опити с експоненциално отдръпване, таймер за повторна проверка и — запазено конкретно за случая, когато ръчният опит на потребителя се провали — съобщение на прост език "изтрий приложението и преинсталирай от уебсайта". Автоматичният път е удобство; ръчният път е гаранцията.

Изводът

  1. Защита около необратимо действие е по-опасна от самото действие. Условното спиране се опитваше да предотврати неуспешно обновяване да спре приложението и вместо това създаде състояние, по-лошо и от спирането, и от неспирането. Пътищата за спиране и инсталация трябва да бъдат безусловни и идемпотентни — никога зависими от променлив флаг, който друг обработчик може да обърне изпод теб.
  2. На macOS "без прозорци" не значи "без приложение." Всяка логика за разрушаване трябва да отчита платформата, където процес без прозорци продължава да работи. Тествай пътя на провала, на реалната ОС, а не само щастливия път.
  3. Функцията, която пускаш чрез системата за обновяване, не може да бъде тествана чрез системата за обновяване. Тази асиметрия е причината автоматичното обновяване да заслужава параноичен, безусловен, силно ръчно проверен код. Получаваш възможността да го поправиш по лесния начин чак след като то вече работи.

Това е най-ранната глава от работата по надеждността, която в крайна сметка стана GeekBye v2. За това докъде доведе този път, виж какво всъщност изисква една втора версия (v2.0.0) и цялата дъга в анатомията на пускането на софтуер до съвършенство.

Свързани статии

Денят, в който приложението ни направи DDoS само на себе си
Steven
Steven5 мин четене

Денят, в който приложението ни направи DDoS само на себе си

Backlog от чакащи качвания, освободен наведнъж при стартиране, превърна всеки GeekBye клиент в малка denial-of-service атака срещу собствените ни сървъри. Фиксът — и стълбата за connection-liveness, която ни принуди да построим — е едно от най-полезните неща, на които ни научи v2.

Надеждност
Мрежи
Инженерство
Какво всъщност изисква една Версия 2: 206 комита честни състояния
Steven
Steven7 мин четене

Какво всъщност изисква една Версия 2: 206 комита честни състояния

GeekBye v2 не беше издание с нови функции. Бяха 206 комита, насочени към една-единствена идея: приложението никога не бива да лъже за собственото си състояние. Ето колко струва това — включително еднолинейната грешка в lockfile, която за малко да ни попречи да пуснем каквото и да било от всичко това.

Надеждност
Инженеринг
Издание
Защо твоят AI notetaker спира записа по средата на срещата
Steven
Steven5 мин четене

Защо твоят AI notetaker спира записа по средата на срещата

Собственото ни приложение прекрати две от нашите срещи, докато отсрещната страна беше по средата на изречение. Криминалистичната следа отведе до добронамерен idle таймер, който не чуваше никого освен теб — и до втори бъг, който можеше да заключи целия ти десктоп. И двата поправени в GeekBye v2.0.9.

Надеждност
Срещи
Инженерство