Steven
Steven6 min leestijd

De veiligheidscontrole die onze app onmogelijk te sluiten maakte

Automatisch updaten was de moeilijkste feature die we ooit hebben uitgebracht — zes releases in vier dagen om te voorkomen dat het de app onbruikbaar maakte. De ergste bug was er een die we zelf introduceerden terwijl we voorzichtig probeerden te zijn: een "veiligheidscontrole" van 500 milliseconden die een mislukte update veranderde in een proces dat je letterlijk niet kon afsluiten.

Engineering
Electron
Betrouwbaarheid
GeekBye Releases
De veiligheidscontrole die onze app onmogelijk te sluiten maakte

Elke ontwikkelaar van desktop-apps onderschat het automatisch updaten precies één keer. Het lijkt een opgelost probleem — een bibliotheek downloadt een nieuwe versie en herstart je app. Dan breng je het uit, en je leert dat "herstart je app" een van de gevaarlijkste dingen is die je een programma kunt vragen, want het gebeurt precies op het moment dat je app zichzelf aan het afbreken is en de minste marge voor fouten heeft.

Het automatisch updaten van GeekBye kostte zes releases in vier dagen — v1.5.14 tot en met v1.5.19 — om te stabiliseren. Dit is het verhaal van de ergste bug in die reeks, die we zelf veroorzaakten door voorzichtig te willen zijn.

Zes releases, en de ene die ertoe deed

De reeks begon alledaags. v1.5.14 verhielp een gênante bug van het typefout-genre: de update-feed wees naar een GitHub-reponaam die niet bestond, dus de updater controleerde een 404. v1.5.15 voegde een handmatige "Check for Updates"-knop en een echte foutmelding toe. Toen begonnen de quitAndInstall-bugs, en de releases kwamen snel — want als je updatemechanisme kapot is, kun je de oplossing ervoor niet via het updatemechanisme uitbrengen. Elke iteratie is een gok met handmatige herinstallatie.

De ene die ertoe doet is v1.5.18. De volledige inhoud was één enkele commit met een titel waar ik nog steeds van ineenkrimp: restore original quitAndInstall behavior to prevent unkillable app.

Hoe "voorzichtig zijn" de app onbruikbaar maakte

Dit is de opzet. Wanneer een update is gedownload, hoort Electrons quitAndInstall de app te sluiten en de nieuwe versie in te wisselen. In een eerdere release maakte iemand — begrijpelijkerwijs — zich zorgen dat onvoorwaardelijk afsluiten riskant was. Wat als de installatie faalde? Zou het niet veiliger zijn om alleen af te sluiten als alles er gezond uitzag?

Dus groeide de code een controle die verstandig leek:

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)

De logica: start de installatie, wacht een halve seconde, en forceer de laatste app.quit() alleen als de updateDownloaded-vlag nog steeds waar is — anders houd je de app open zodat de gebruiker niet gestrand raakt.

De valkuil ligt één regel verderop, in de foutafhandelaar. Die afhandelaar zette this.updateDownloaded = false. Stel je dus een mislukte installatie voor: het error-event vuurt en wist de vlag. Maar quitAndInstall was al begonnen met de afbraak — het had de vensters gesloten en de afsluitlisteners van de app verwijderd. Dan ontwaakt de timer van 500 ms, controleert de nu-onware vlag, besluit "fout gedetecteerd, houd de app open," en slaat app.quit() over.

Nu heb je, specifiek op macOS, de slechtst mogelijke toestand. macOS sluit een app niet af alleen omdat het laatste venster is gesloten — dat is het window-all-closed-gedrag waar elke Mac-app op rekent. Dus het proces leeft nog, maar het heeft geen venster, geen pad via de menubalk, en zijn afsluitlisteners zijn eruit gerukt. Er is niets om op te klikken. Cmd-Q heeft niets om mee te praten. De enige uitweg is Force Quit vanuit Activity Monitor. De "veiligheids"-controle had een mislukte update — een herstelbaar ongemak — omgezet in een zombie die je niet kon doden.

De oplossing: afbraak moet onvoorwaardelijk zijn

De correctie in v1.5.18 is bijna agressief saai, en dat is het punt. Ze haalt de slimmigheid weg:

  1. Verwijder de window-all-closed- en before-quit-listeners die konden interfereren.
  2. Vernietig elk venster — window.destroy(), niet window.close(). Sluiten kan door een handler worden geblokkeerd; vernietigen niet. Als je je vastlegt op afsluiten, vraag je niet beleefd.
  3. Roep quitAndInstall aan.
  4. Roep app.quit() onvoorwaardelijk aan.

Geen vlag, geen timer, geen "houd het voor de zekerheid open." Want de waarheid over een afsluitpad is dat een half afgemaakte afsluiting erger is dan beide uitkomsten. Volledig afsluiten is prima. Volledig openblijven is prima. De ene toestand die je nooit mag bereiken is afgebroken maar nog draaiend — en dat is precies de toestand waarin een voorwaardelijke afsluiting je kan laten stranden.

Twee lessen meer die dezelfde week ons leerde

De onstopbare bug is de kop, maar het zes-releases-geharrewar verstevigde nog twee andere gewoontes die het stelen waard zijn.

Filter je crash-telemetrie op exacte handtekeningen, niet op brede sleutelwoorden. Midden in het geharrewar ontdekten we dat onze foutrapportage zo was ingesteld dat alles met woorden als permission, token of microphone werd weggegooid — een poging om ruis te snijden die stilletjes echte crashes verzwolg die toevallig die woorden noemden. We rukten de blanco filters eruit en verving ze door exacte weigeringsstrings (het specifieke bericht dat macOS uitzendt wanneer een permissie wordt geweigerd) en specifieke tijdelijke netwerkcodes zoals ERR_NETWORK_CHANGED. Ruisreductie en bugs-verbergen zijn dezelfde knop, tegengesteld gedraaid; als je op gevoel filtert, filter je het ding weg dat je nodig had te zien.

Elk automatisch pad heeft een handmatige nooduitgang nodig. Automatisch updaten is van nature best-effort — netwerken haperen, installaties falen. Dus elke foutmodus kreeg een menselijke terugvaloptie: de handmatige "Check for Updates"-knop, herpogingen met exponentiële backoff, een hercontrole-timer, en — specifiek gereserveerd voor het geval waarin de handmatige poging van een gebruiker faalde — een begrijpelijk "verwijder de app en herinstalleer vanaf de website"-bericht. Het automatische pad is een gemak; het handmatige pad is de garantie.

De conclusie

  1. Een controle rond een onomkeerbare actie is gevaarlijker dan de actie zelf. De voorwaardelijke afsluiting probeerde te voorkomen dat een slechte update de app afsloot, en creëerde in plaats daarvan een toestand die erger was dan zowel afsluiten als niet afsluiten. Afsluit- en installatiepaden horen onvoorwaardelijk en idempotent te zijn — nooit afhankelijk van een veranderbare vlag die een andere handler onder je vandaan kan omzetten.
  2. Op macOS betekent "geen vensters" niet "geen app." Elke afbraaklogica moet rekening houden met het platform waar een vensterloos proces blijft draaien. Test het foutpad, op het echte besturingssysteem, niet alleen het gelukkige pad.
  3. De feature die je uitbrengt via het updatesysteem kan niet worden getest via het updatesysteem. Die asymmetrie is waarom automatisch updaten paranoïde, onvoorwaardelijke, zwaar handmatig geverifieerde code verdient. Je mag het pas op de makkelijke manier repareren nadat het al werkt.

Dit is het vroegste hoofdstuk van het betrouwbaarheidswerk dat uiteindelijk GeekBye v2 werd. Voor waar die weg heen leidde, zie wat een versie 2 echt kost (v2.0.0) en de hele reeks in de anatomie van software tot in de perfectie uitbrengen.

Gerelateerde Artikelen

Een verbroken verbinding hoort niet je hele app te laten crashen — de onze deed dat wel
Steven
Steven6 min leestijd

Een verbroken verbinding hoort niet je hele app te laten crashen — de onze deed dat wel

Toen onze backend midden in een meeting offline ging, pauzeerde niet alleen de transcriptie — de hele app crashte. De oorzaak was één onbehandeld event, en de fix was tien regels. Dit is het releasecluster dat GeekBye ingelogd én verbonden liet blijven door de dingen die dat vroeger de kop kostten.

Engineering
Betrouwbaarheid
Electron
De stilte was dragend
Steven
Steven8 min leestijd

De stilte was dragend

De laatste twee releases van GeekBye v1 gaan over dezelfde ongemakkelijke waarheid: real-time transcriptie over een echt netwerk is niet verliesvrij, en de eerlijke zet is te stoppen met doen alsof het dat wel is. v1.8.20 bewaarde een kopie van elke audiochunk op schijf voordat het die tijdens een reconnect weggooide, en begon de gaten in het transcript hardop te markeren. v1.9.0 stopte met het versturen van stilte om bandbreedte te besparen — en ontdekte dat de stilte precies het signaal was dat de transcriber gebruikte om te weten dat een zin was afgelopen. Twee releases over de kosten van dingen weggooien.

Engineering
Audio
Betrouwbaarheid
Een meeting naar PDF printen zonder PDF-bibliotheek
Steven
Steven9 min leestijd

Een meeting naar PDF printen zonder PDF-bibliotheek

GeekBye exporteert een meeting als PDF, en er staat nergens in de code een PDF-bibliotheek. Het rendert HTML in een onzichtbaar browservenster en print dat. Die keuze is het hele verhaal: het maakte de functie makkelijk te bouwen en gaf hem elke storing die een echte browser heeft — een witte flits, een limiet op de URL-lengte, en een pagina-einde dat screenshots doormidden sneed. De fix voor de lelijkste bug was één regel CSS.

Engineering
Electron
Desktop