Steven
Steven6 min di lettura

Il controllo di sicurezza che ha reso la nostra app impossibile da chiudere

L'aggiornamento automatico è stata la funzionalità più difficile che abbiamo mai rilasciato — sei versioni in quattro giorni per evitare che rendesse l'app inutilizzabile. Il bug peggiore è stato uno che abbiamo introdotto noi stessi cercando di essere prudenti: un controllo "di sicurezza" da 500 millisecondi che trasformava un aggiornamento fallito in un processo che, letteralmente, non potevi chiudere.

Ingegneria
Electron
Affidabilità
Rilasci di GeekBye
Il controllo di sicurezza che ha reso la nostra app impossibile da chiudere

Ogni sviluppatore di app desktop sottovaluta l''aggiornamento automatico esattamente una volta. Sembra un problema risolto — una libreria scarica una nuova versione e riavvia la tua app. Poi lo rilasci, e scopri che "riavvia la tua app" è una delle cose più pericolose che si possano chiedere a un programma, perché accade nel momento esatto in cui la tua app si sta smontando da sola e ha il minor margine di errore possibile.

L''aggiornamento automatico di GeekBye ha richiesto sei versioni in quattro giorni — dalla v1.5.14 alla v1.5.19 — per stabilizzarsi. Questa è la storia del bug peggiore di quel tratto, che abbiamo causato noi stessi cercando di essere prudenti.

Sei versioni, e quella che contava

L''arco è iniziato in modo banale. La v1.5.14 ha corretto un bug da errore di battitura imbarazzante: il feed degli aggiornamenti puntava a un nome di repository GitHub che non esisteva, quindi l''updater interrogava un 404. La v1.5.15 ha aggiunto un pulsante manuale "Check for Updates" e un vero messaggio di errore. Poi sono iniziati i bug di quitAndInstall, e le versioni sono arrivate in fretta — perché quando il tuo meccanismo di aggiornamento è rotto, non puoi rilasciarne la correzione attraverso il meccanismo di aggiornamento. Ogni iterazione è una scommessa di reinstallazione manuale.

Quella che conta è la v1.5.18. Tutto il suo contenuto era un singolo commit con un titolo che ancora mi fa rabbrividire: ripristinare il comportamento originale di quitAndInstall per evitare un''app impossibile da terminare.

Come "essere prudenti" ha reso l''app inutilizzabile

Ecco lo scenario. Quando un aggiornamento è scaricato, il quitAndInstall di Electron dovrebbe chiudere l''app e sostituirla con la nuova versione. In una versione precedente, qualcuno — ragionevolmente — si è preoccupato che chiudere incondizionatamente fosse rischioso. E se l''installazione dava errore? Non sarebbe più sicuro chiudere solo se tutto sembrava a posto?

Così il codice si è arricchito di una guardia che sembrava sensata:

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)

La logica: avvia l''installazione, aspetta mezzo secondo, e forza il app.quit() finale solo se il flag updateDownloaded è ancora vero — altrimenti tieni l''app aperta così l''utente non resta a piedi.

La trappola è a una riga di distanza, nel gestore degli errori. Quel gestore impostava this.updateDownloaded = false. Immagina quindi un''installazione fallita: l''evento error scatta e azzera il flag. Ma quitAndInstall aveva già avviato lo smontaggio — aveva chiuso le finestre e rimosso i listener di chiusura dell''app. Poi il timer da 500ms si sveglia, controlla il flag ora falso, decide "errore rilevato, tieni l''app aperta" e salta app.quit().

Ora hai, su macOS in particolare, il peggior stato possibile. macOS non chiude un''app solo perché la sua ultima finestra si è chiusa — è il comportamento window-all-closed su cui si appoggia ogni app Mac. Quindi il processo è ancora vivo, ma non ha finestra, né un percorso dalla barra dei menu, e i suoi listener di chiusura sono stati strappati via. Non c''è niente su cui cliccare. Cmd-Q non ha nessuno con cui parlare. L''unica uscita è Force Quit da Activity Monitor. Il controllo "di sicurezza" aveva trasformato un aggiornamento fallito — una seccatura recuperabile — in uno zombie che non potevi uccidere.

La correzione: lo smontaggio deve essere incondizionato

La correzione nella v1.5.18 è quasi aggressivamente noiosa, ed è proprio questo il punto. Rimuove la furbizia:

  1. Rimuovere i listener window-all-closed e before-quit che potevano interferire.
  2. Distruggere ogni finestra — window.destroy(), non window.close(). La chiusura può essere vetata da un gestore; la distruzione no. Quando ti stai impegnando a spegnere, non lo chiedi con gentilezza.
  3. Chiamare quitAndInstall.
  4. Chiamare app.quit() incondizionatamente.

Nessun flag, nessun timer, nessun "tienila aperta per sicurezza". Perché la verità su un percorso di spegnimento è che uno spegnimento a metà è peggio di entrambi i risultati. Chiudere del tutto va bene. Restare del tutto aperti va bene. L''unico stato che non devi mai raggiungere è smontato ma ancora in esecuzione — ed è esattamente lo stato in cui una chiusura condizionata può lasciarti a piedi.

Altre due lezioni che ha insegnato la stessa settimana

Il bug dell''app impossibile da terminare è il titolo, ma la corsa alle sei versioni ha temprato altre due abitudini che vale la pena rubare.

Filtra la tua telemetria dei crash per firme esatte, non per parole chiave ampie. A metà della corsa abbiamo scoperto che il nostro reporting degli errori era configurato per scartare qualsiasi cosa contenesse parole come permission, token o microphone — un tentativo di tagliare il rumore che stava inghiottendo silenziosamente crash reali che per caso menzionavano quelle parole. Abbiamo strappato via i filtri generici e li abbiamo sostituiti con stringhe di rifiuto esatte (il messaggio specifico che macOS emette quando un permesso viene negato) e codici di rete transitori specifici come ERR_NETWORK_CHANGED. Riduzione del rumore e occultamento dei bug sono la stessa manopola girata in direzioni opposte; se filtri a sensazione, filtrerai proprio la cosa che avevi bisogno di vedere.

Ogni percorso automatico ha bisogno di una via di fuga manuale. L''aggiornamento automatico è per natura al meglio delle possibilità — le reti vacillano, le installazioni falliscono. Così ogni modalità di guasto ha ricevuto un ripiego umano: il pulsante manuale "Check for Updates", tentativi con backoff esponenziale, un timer di ri-verifica e — riservato specificamente al caso in cui il tentativo manuale di un utente falliva — un messaggio in linguaggio semplice "elimina l''app e reinstallala dal sito web". Il percorso automatico è una comodità; il percorso manuale è la garanzia.

La conclusione

  1. Una guardia attorno a un''azione irreversibile è più pericolosa dell''azione stessa. La chiusura condizionata ha cercato di impedire che un aggiornamento difettoso chiudesse l''app, e invece ha creato uno stato peggiore sia del chiudere sia del non chiudere. I percorsi di spegnimento e installazione dovrebbero essere incondizionati e idempotenti — mai vincolati a un flag mutabile che un altro gestore può ribaltarti sotto i piedi.
  2. Su macOS, "nessuna finestra" non è "nessuna app". Ogni logica di smontaggio deve tenere conto della piattaforma dove un processo senza finestre continua a girare. Testa il percorso del guasto, sul sistema operativo reale, non solo il percorso felice.
  3. La funzionalità che rilasci attraverso il sistema di aggiornamento non può essere testata attraverso il sistema di aggiornamento. Questa asimmetria è il motivo per cui l''aggiornamento automatico merita codice paranoico, incondizionato e verificato a mano a fondo. Puoi ripararlo per la via facile solo dopo che già funziona.

Questo è il primo capitolo del lavoro di affidabilità che alla fine è diventato GeekBye v2. Per vedere dove ha portato quella strada, guarda cosa serve davvero per una versione 2 (v2.0.0) e l''intero arco in l''anatomia del rilasciare software alla perfezione.

Articoli Correlati

Una connessione caduta non dovrebbe mandare in crash tutta la tua app — ma la nostra lo faceva
Steven
Steven7 min di lettura

Una connessione caduta non dovrebbe mandare in crash tutta la tua app — ma la nostra lo faceva

Quando il nostro backend è andato offline nel mezzo di una riunione, non ha solo messo in pausa la trascrizione — ha mandato in crash l'intera app. La causa era un singolo evento non gestito, e la correzione erano dieci righe. Questo è il gruppo di release che ha reso GeekBye capace di restare connesso all'account e restare connesso alla rete attraverso le cose che prima lo uccidevano.

Ingegneria
Affidabilità
Electron
Cosa serve davvero per una versione 2: 206 commit di stati onesti
Steven
Steven8 min di lettura

Cosa serve davvero per una versione 2: 206 commit di stati onesti

La v2 di GeekBye non è stata un rilascio di funzionalità. Sono stati 206 commit puntati a una sola idea: l'app non deve mai mentire sul proprio stato. Ecco quanto costa — incluso l'errore di una riga nel lockfile che per poco non ci ha impedito di rilasciare tutto.

Affidabilità
Ingegneria
Rilascio
Il silenzio era portante
Steven
Steven8 min di lettura

Il silenzio era portante

Le ultime due release di GeekBye v1 ruotano attorno alla stessa verità scomoda: la trascrizione in tempo reale su una rete vera non è senza perdite, e la mossa onesta è smettere di fingere che lo sia. La v1.8.20 teneva su disco una copia di ogni chunk audio prima di scartarlo durante una riconnessione, e ha iniziato a segnare ad alta voce i buchi nella trascrizione. La v1.9.0 ha smesso di inviare silenzio per risparmiare banda — e ha scoperto che il silenzio era proprio il segnale che il trascrittore usava per sapere che una frase era finita. Due release sul costo di buttare via le cose.

Ingegneria
Audio
Affidabilità