Steven
Steven6 хв читання

Розрив з'єднання не повинен ронити весь застосунок — а наш ронив

Коли наш бекенд відвалився посеред зустрічі, він не просто призупинив транскрипцію — він поклав увесь застосунок. Причиною була одна необроблена подія, а виправлення вмістилося в десять рядків. Це кластер релізів, завдяки якому GeekBye лишається в системі й на зв'язку крізь те, що раніше його вбивало.

Інженерія
Надійність
Electron
Релізи GeekBye
Розрив з'єднання не повинен ронити весь застосунок — а наш ронив

Деякі баги переривають функцію. Найгірші валять усе довкола неї. У ранньому білді GeekBye, якщо наш бекенд відвалювався, поки ви були посеред зустрічі, транскрипція не просто зупинялася — падав увесь застосунок. Один розрив з'єднання — і все вікно зникало.

Кластер релізів, який це полагодив — з v1.6.8 по v1.6.11 — це майстер-клас непоказної роботи в дусі «лишайся в системі, лишайся на зв'язку». Заголовне виправлення вмістилося в десять рядків. Ось чим були ці десять рядків і все інше, що вийшло разом з ними.

Десять рядків між розривом і падінням

Падіння жило в аудіокоді. Коли сесія транскрипції розбирала свої WebSocket, вона викликала на них removeAllListeners() — розумне прибирання. Але ось деталь Node.js, яка перетворює прибирання на катастрофу: подія error без слухача не ігнорується. Вона перекидається як непіймана виняткова ситуація.

Тож уявіть послідовність, коли бекенд відвалюється: у сокета є подія error, поставлена в чергу на спрацювання. Прибирання запускає removeAllListeners(), знімаючи обробник, який би її піймав. Мить по тому поставлена в чергу помилка спрацьовує — у порожнечу. Node бачить подію error, яку ніхто не слухає, і робить єдине, що дозволяє його контракт: перекидає її як непійману виняткову ситуацію, що кладе процес. Бекенд користувача гикнув на дві секунди — і його застосунок зник.

Виправлення (v1.6.8) майже антикульмінаційне. Одразу після зняття слухачів знову навісьте навмисно порожній обробник error:

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

Ця порожня функція — увесь сенс. Вона дає помилці, що чекає, місце, куди приземлитися — приймач — тож у Node є слухач, і воно ніколи не перекидає заново. Той самий коміт також прибрав напіввідкриті сокети, коли спроба підключення провалюється на півдорозі, щоб зіпсоване підключення не могло лишити живий сокет теліпатися з помилкою, що чекає. Коментар до коміту каже прямо: «Без цього помилки, що чекають, стають непійманими винятковими ситуаціями й кладуть застосунок.» Десять рядків перетворили повне падіння на тихий, відновлюваний розрив.

Урок вартий більше, ніж виправлення: щоразу, коли ви викликаєте removeAllListeners() на сокеті чи потоці, який, можливо, ще розбираєте, спершу знову навісьте порожній приймач error. Необроблена подія error — це падіння, а не рядок у лозі.

Лишатися в системі: онови завчасно й онови на відскоку

У половини «лишайся на зв'язку» був близнюк — половина «лишайся в системі». До цього кластера токен автентифікації GeekBye просто спливав — за якийсь час вас без церемоній викидало назад на екран входу, посеред роботи, без видимої причини.

v1.6.8 полагодив це з обох боків:

  • Проактивно: таймер оновлює токен до спливання, запланований на момент спливання мінус розумний буфер (частка часу життя токена, обмежена знизу й згори). Вас оновлюють раніше, ніж ви взагалі помітите.
  • Реактивно: якщо запит усе одно повертається з 401 — зсув годинника, пропущений таймер, вихолоджений ноутбук — клієнт оновлює токен один раз і повторює вихідний запит рівно один раз. Єдиний прапорець-сторож обмежує цей повтор, тож упертий 401 ніколи не зможе розкрутитися в нескінченний цикл.

Тонка, але критична частина: обидві гілки стоять за м'ютексом. Якщо десять запитів упираються в 401 тієї самої миті, вони не запускають десять оновлень — вони всі чекають на одне оновлення в польоті, а потім повторюють. А коли оновлення справді неможливо врятувати, застосунок завершує сесію плавно — зрозуміле десятисекундне сповіщення «ваша сесія сплила, увійдіть знову» — замість тихого стусана на екран входу.

Перепідключення: додай його на клієнті, потім перенеси на бекенд

v1.6.9 зробив розірване з'єднання транскрипції відновлюваним замість фатального. Коли бекенд повідомляв, що висхідне мовленнєве з'єднання обірвалося, клієнт переставав видавати фатальну помилку й натомість перепідключався — з навмисно спокійним UX: одне бурштинове сповіщення «Перепідключення…» (не по одному на спробу), зелене сповіщення «Перепідключено», коли воно поверталося, і термінальна помилка «будь ласка, перезапустіть» лише після того, як вичерпано кожну спробу.

Потім те, що мені подобається в цьому кластері: релізом пізніше, у v1.6.10, ми видалили цю логіку перепідключення на боці клієнта — близько 113 рядків — і перенесли перепідключення на бекенд, який тепер випромінює тихі повідомлення статусу «перепідключення», які клієнт просто відображає. Ми побудували відновлення на клієнті, вирішили, що бекенд — правильний власник, і перенесли його. Це не хитання; це чесний життєвий цикл важкої задачі. Логіка надійності, розмазана по обох кінцях з'єднання, — це поганий запах: виберіть власника. (Бекенд зрештою став остаточним власником цього, що і є історією перепідключення, розказаною в чому ваш ШІ-нотатник зупиняється на поганому Wi-Fi.)

Той самий реліз зробив ліміти часу запису людяними: замість сирої помилки — попереджувальне сповіщення, яке дозволяє запису тривати далі, і зрозуміле повідомлення «Досягнуто ліміт запису», коли він справді зупиняється — дружня фраза винесена назовні, внутрішній префікс прибрано.

Один повтор, щоб керувати всіма

До v1.6.11 та сама логіка повтору з бекофом була скопійована в три різні місця — автентифікація, оновлення, розбір сесії. Тож її консолідували в один модуль з одним спільним правилом про те, що взагалі варто повторювати: перехідні мережеві збої (скидання з'єднання, тайм-аут, гикавка DNS, обрив сокета) повторюються з експоненційним бекофом; справжня помилка сервера падає швидко, бо повтор автентичного 4xx лише марнує час користувача. Один класифікатор, кілька іменованих пресетів, три викликачі, перенесені на нього.

Чого навчив цей кластер

  1. Необроблена подія error — це падіння, а не рядок у лозі. Node перекидає події error, у яких немає слухача. Зніміть слухачів із сокета, який, можливо, ще розбираєте, і ви зобов'язані знову навісити порожній приймач error — інакше один розрив з'єднання забере із собою весь застосунок.
  2. Онови облікові дані до спливання і реактивно при 401 — з м'ютексом на обох. Проактивне тримає вас у системі; реактивне ловить крайові випадки; м'ютекс означає, що N одночасних викликачів спричиняють рівно одне оновлення, а ліміт повторів означає, що поганий токен не може зациклитися назавжди.
  3. Повторюй лише при перехідних помилках і централізуй класифікатор. «Чи варто це повторювати?» — це одне рішення, яке належить одному місцю. Повтор справжньої помилки — це просто повільніший провал.
  4. Виріши, хто володіє перепідключенням. Ми додали його на клієнті, потім перенесли на бекенд. Логіка відновлення, що живе на обох кінцях, — це генератор багів; дайте їй єдиний дім.

Це третій розділ історії про надійність, яка стає GeekBye v2. Про попередній розділ див. ми видалили 5000 рядків аудіокоду, і транскрипції почали з'являтися двічі (v1.6.0); про те, де перепідключення зрештою осіло, чому ваш ШІ-нотатник зупиняється на поганому Wi-Fi; а про всю дугу, анатомію доведення софту до досконалості.

Схожі статті

Перевірка безпеки, через яку застосунок стало неможливо закрити
Steven
Steven5 хв читання

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

Автооновлення виявилося найважчою функцією, яку ми будь-коли випускали, — шість релізів за чотири дні, щоб воно перестало ламати застосунок. Найгіршу ваду ми внесли самі, намагаючись бути обережними: 500-мілісекундна «захисна» перевірка, що перетворювала невдале оновлення на процес, який буквально неможливо було закрити.

Інженерія
Electron
Надійність
День, коли наш застосунок влаштував DDoS самому собі
Steven
Steven5 хв читання

День, коли наш застосунок влаштував DDoS самому собі

Черга відкладених завантажень, випущена вся разом при запуску, перетворила кожен клієнт GeekBye на маленьку атаку denial-of-service на наші власні сервери. Виправлення — і драбина liveness з’єднання, яку воно змусило нас побудувати — одна з найкорисніших речей, яких навчив нас v2.

Надійність
Мережі
Інженерія
Чого насправді варта версія 2: 206 комітів чесних станів
Steven
Steven6 хв читання

Чого насправді варта версія 2: 206 комітів чесних станів

GeekBye v2 не був релізом нових функцій. Це були 206 комітів, націлених на одну ідею: застосунок ніколи не повинен брехати про власний стан. Ось чого це коштує — включно з однорядковою помилкою в lockfile, яка ледь не завадила нам випустити хоч щось із цього.

Надійність
Інженерія
Реліз