Steven
Steven6 мин четене

Прекъснатата връзка не бива да срива цялото приложение — но нашето го правеше

Когато бекендът ни падна по средата на среща, той не просто спря транскрипцията — срина цялото приложение. Причината беше едно необработено събитие, а поправката — десет реда. Това е групата от версии, която накара GeekBye да остане в профила и да остане свързан въпреки нещата, които преди го убиваха.

Инженерство
Надеждност
Electron
Версии на GeekBye
Прекъснатата връзка не бива да срива цялото приложение — но нашето го правеше

Някои бъгове прекъсват една функция. Най-лошите събарят всичко около нея. В ранна версия на GeekBye, ако бекендът ни паднеше, докато бяхте по средата на среща, транскрипцията не просто спираше — цялото приложение се сриваше. Една прекъсната връзка и целият прозорец изчезваше.

Групата от версии, която поправи това — от v1.6.8 до v1.6.11 — е майсторски урок по неблагодарната работа „остани в профила, остани свързан". Водещата поправка беше десет реда. Ето какви бяха тези десет реда и всичко останало, което излезе заедно с тях.

Десет реда между прекъсване и срив

Сривът живееше в аудио кода. Когато сесия за транскрипция разграждаше своите WebSocket-и, тя извикваше removeAllListeners() върху тях — разумно почистване. Но ето детайла в Node.js, който превръща почистването в катастрофа: събитие error без слушател не се игнорира. То се хвърля повторно като неуловено изключение.

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

Поправката (v1.6.8) е почти антикулминационна. Веднага след премахването на слушателите, прикачваш отново умишлен no-op обработчик на error:

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

Тази празна функция е целият смисъл. Тя дава на чакащата грешка място, където да кацне — приемник — така че Node има слушател и никога не хвърля повторно. Същият комит също почисти полуотворените сокети, когато опит за свързване се провали по средата, така че несполучлив connect да не може да остави жив сокет да виси с грешка, готова да се задейства. Коментарът към комита го казва ясно: „Без това чакащите грешки стават неуловени изключения и сриват приложението." Десет реда превърнаха пълен срив в тихо, възстановимо прекъсване.

Урокът струва повече от поправката: всеки път, когато извикваш removeAllListeners() върху сокет или поток, който може все още да разграждаш, първо прикачи отново no-op приемник за error. Необработено събитие error е срив, а не ред в лога.

Оставане в профила: опресни предварително и опресни при отскока

Половината „остани свързан" имаше близнак „остани в профила". Преди тази група токенът за автентикация на GeekBye просто изтичаше — след време ви изхвърляше безцеремонно обратно на екрана за вход, по средата на работния поток, без видима причина.

v1.6.8 поправи това от двете посоки:

  • Проактивно: таймер опреснява токена преди да изтече, планиран на изтичане минус умен буфер (част от живота на токена, с долна граница и таван). Опреснени сте, преди изобщо да забележите.
  • Реактивно: ако заявка все пак се върне с 401 — изместване на часовника, пропуснат таймер, студен лаптоп — клиентът опреснява веднъж и повтаря оригиналната заявка точно веднъж. Един защитен флаг ограничава този повторен опит, така че траен 401 никога да не може да се завърти в безкраен цикъл.

Тънката, но критична част: и двата пътя са зад mutex. Ако десет заявки ударят 401 в един и същи миг, те не задействат десет опреснявания — всички изчакват единственото опресняване в ход и после повтарят. А когато опресняване наистина не може да бъде спасено, приложението прекратява сесията плавно — ясно десетсекундно известие „сесията ви изтече, моля влезте отново" — вместо тих ритник към екрана за вход.

Повторно свързване: добави го на клиента, после го премести на бекенда

v1.6.9 направи прекъсната връзка за транскрипция възстановима вместо фатална. Когато бекендът докладваше, че връзката за реч нагоре по веригата е прекъсната, клиентът спираше да показва фатална грешка и вместо това се свързваше отново — с умишлено спокоен UX: едно кехлибарено известие „Повторно свързване…" (не по едно на опит), зелено известие „Свързано отново", когато се върнеше, и крайната грешка „моля рестартирайте" само след като всеки опит беше изчерпан.

После нещо, което харесвам в тази група: една версия по-късно, в v1.6.10, изтрихме тази логика за повторно свързване от страна на клиента — около 113 реда — и преместихме повторното свързване на бекенда, който сега излъчва тихи статусни съобщения „повторно свързване", които клиентът просто отразява. Изградихме възстановяването на клиента, решихме, че бекендът е правилният собственик, и го преместихме. Това не е колебание; това е честният жизнен цикъл на труден проблем. Логика за надеждност, разделена между двата края на връзка, е лош знак — избери собственик. (Бекендът в крайна сметка стана окончателният собственик на това, което е историята за повторно свързване, разказана в защо вашият AI бележник спира при лош Wi-Fi.)

Същата версия направи лимитите за време на запис човечни: вместо груба грешка, предупредително известие, което позволява записът да продължи, и ясно съобщение „Достигнат лимит на записа", когато той наистина спре — приятелското изречение изведено на повърхността, вътрешният префикс премахнат.

Един повторен опит, който да ги управлява всички

До v1.6.11 същата логика за повторен опит с backoff беше копирана и поставена на три различни места — автентикация, обновявания, разграждане на сесия. Затова беше консолидирана в един модул с едно споделено правило за какво изобщо си струва да се повтаря: преходните мрежови повреди (нулиране на връзка, timeout, засечка на DNS, прекъснат сокет) се повтарят с експоненциален backoff; истинска сървърна грешка се проваля бързо, защото повторното опитване на истински 4xx просто губи времето на потребителя. Един класификатор, няколко именувани пресета, трима извикващи, мигрирани към него.

Какво ни научи тази група

  1. Необработено събитие error е срив, а не ред в лога. Node хвърля повторно събитията error, които нямат слушател. Премахни слушателите на сокет, който може още да разграждаш, и трябва да прикачиш отново no-op приемник за грешки — иначе една прекъсната връзка отнася цялото приложение със себе си.
  2. Опреснявай удостоверенията преди изтичане и реактивно при 401 — с mutex и на двете. Проактивното те държи в профила; реактивното улавя гранични случаи; mutex-ът означава, че N едновременни извикващи причиняват точно едно опресняване, а таван на повторните опити означава, че лош токен не може да зацикли завинаги.
  3. Повтаряй само при преходни грешки и централизирай класификатора. „Струва ли си да се повтаря това?" е едно решение, което принадлежи на едно място. Повторното опитване на истинска грешка е просто по-бавен провал.
  4. Реши кой притежава повторното свързване. Добавихме го на клиента, после го преместихме на бекенда. Логика за възстановяване, живееща на двата края, е генератор на бъгове; дай ѝ един дом.

Това е третата глава от историята за надеждността, която става GeekBye v2. За предишната глава вижте изтрихме 5000 реда аудио код и транскрипциите започнаха да се появяват двойно (v1.6.0); за това къде в крайна сметка се установи повторното свързване, защо вашият AI бележник спира при лош Wi-Fi; а за цялата дъга, анатомията на изпращането на софтуер до съвършенство.

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

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

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

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

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

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

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

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

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

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

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