Sviluppo
Caricare un file è più difficile di quanto sembri
Tre incidenti veri sul caricamento documenti, e la regola che ne è uscita: la promessa fatta all'utente dev'essere durevole quanto i suoi dati.
Un utente sceglie un PDF, preme Salva, legge "Documento caricato". Sembra la funzione più banale che ci sia.
In Konnexa quel percorso lo abbiamo riscritto tre volte in quattro mesi, e ogni volta perché qualcuno aveva letto "caricato" per un file che non c'era. Quello che segue è cosa è andato storto, in ordine, e la regola sola che ne è uscita.
Serve un pezzo di contesto: in Konnexa i documenti non stanno sul server dell'applicazione. Vivono su un servizio separato, che chiamiamo Cloud, e ci si parla via HTTP. È una scelta che paga — i file sopravvivono al deploy, si replicano, non riempiono il disco dell'app — ma sposta il problema: fra il "Salva" dell'utente e il file effettivamente scritto ci sono adesso una rete e un'altra macchina.
Primo: il 200 che mentiva
Il Cloud rispondeva HTTP 200. Il codice, ragionevolmente, considerava l'upload
riuscito, scriveva il documento in anagrafica e diceva all'utente che era fatta.
Poi qualcuno ha aperto un allegato e ha trovato un file da zero byte.
Il Cloud aveva creato il record del file e restituito 200, ma i byte fisici non erano mai stati scritti. La risposta era formalmente corretta: era la nostra lettura a essere sbagliata. Avevamo trattato "la richiesta è stata accettata" come "il file esiste", che sono due affermazioni diverse — e in un sistema distribuito sono diverse sempre, non solo quando qualcosa si rompe.
La correzione è banale una volta vista: non fidarsi del codice di stato, guardare
cosa dice la risposta. Se torna indietro size = 0, l'upload è fallito anche se
il numero in cima era verde. Da lì, tre tentativi prima di arrendersi.
Un codice di stato dice com'è andata la conversazione, non com'è andato il lavoro. Sono due domande diverse, e conviene farle entrambe.
Secondo: i byte nel messaggio
Sistemata l'affidabilità, restava la lentezza. Salvare quattro dipendenti con i loro documenti voleva dire quattro chiamate di rete in fila, con l'utente fermo a guardare la rotella.
Soluzione ovvia: metterli in coda. L'utente riceve subito la risposta, un processo in sottofondo carica i file con calma. Abbiamo messo il contenuto del file dentro il messaggio della coda e siamo andati avanti.
Il primo salvataggio in blocco con quattro documenti è morto così:
Allowed memory size of 134217728 bytes exhausted
I byte si erano moltiplicati per strada. Codificati per il trasporto (+33%), serializzati dentro la busta del messaggio, e — in sviluppo — clonati un'altra volta ancora dagli strumenti di debug che fotografano ogni richiesta. Un allegato da qualche megabyte era diventato un multiplo di sé stesso, in memoria, tutto insieme.
Il dettaglio peggiore non è l'errore: è dove cadeva. I dipendenti erano già salvati correttamente, ma la risposta non partiva più e la pagina restava immobile. Dal lato dell'utente sembrava che non fosse successo niente, mentre era successo quasi tutto.
La correzione: nel messaggio ci va l'identificativo, non il contenuto. I byte si scrivono una volta in un posto solo e chi li deve leggere se li va a prendere. Il messaggio torna a pesare quanto un numero.
Terzo: il posto dove avevamo messo i byte
E qui arriva quello che ci ha insegnato di più.
I byte erano finiti in una cartella temporanea sul disco del server. Ragionevole: sono di passaggio, li si legge e li si butta.
Il 25 agosto un'azienda ha caricato quattro documenti. Il processo in sottofondo era fermo — un intoppo dopo un deploy, di quelli che capitano — e nessuno se n'era accorto. Nel frattempo il sistema ha fatto quello che fa sempre: ha ripulito la cartella temporanea.
Il 27 agosto il processo è ripartito, ha letto i suoi messaggi e ha trovato quattro percorsi che puntavano al nulla.
Unable to open path .../var/temp/unilav_*.pdf
Due giorni prima quell'utente aveva letto "Documento aggiornato con successo".
Nessuno dei tre pezzi si era comportato male. Il deploy fa ripartire i processi. Il sistema pulisce i file temporanei. La coda conserva i messaggi. Ognuno faceva il suo mestiere: solo che il messaggio era durevole e il file no, e nessuno aveva mai scritto da nessuna parte che quei due dovevano durare la stessa cosa.
Adesso i byte stanno nella stessa base dati che contiene la coda. Un messaggio è autosufficiente: contiene, o sa raggiungere, tutto ciò che gli serve. Sopravvive a un riavvio, a un deploy, a un container ricreato da zero e a un processo fermo per giorni. La riga si cancella solo quando il Cloud ha confermato di aver scritto davvero — con il controllo del primo incidente, che è rimasto lì a fare la guardia.
La regola che ne è uscita
Tutti e tre gli incidenti sono lo stesso incidente, raccontato da tre angoli:
La promessa fatta all'utente deve essere durevole quanto il posto in cui hai messo i suoi dati.
Se dici "caricato", il file deve esserci. Se non puoi ancora dirlo, di' un'altra cosa. Oggi Konnexa infatti scrive "Documento acquisito: sarà disponibile tra poco", che è più lungo, meno soddisfacente da leggere — ed è vero. Se qualcosa si inceppa, la riga resta segnata come da sistemare e si vede in pagina, invece di sparire in silenzio.
Ed è anche il motivo per cui non abbiamo festeggiato troppo presto: dei tre problemi, due li ha visti un utente prima di noi. Un upload che fallisce in modo rumoroso è un fastidio; uno che fallisce dicendo che è andato bene è un documento che non esiste e che nessuno cercherà finché non serve davvero — cioè, per un documento della sicurezza, nel momento peggiore possibile.
Se ti occupi di sicurezza sul lavoro e il problema è il contrario — i documenti ci sono ma nessuno sa quando scadono — ne parliamo qui.