Dal codice a velocità di macchina non esci con la review

Questa serie ha portato avanti un solo argomento con quattro travestimenti. La complessità che non governi non sparisce, si accumula. Scrivi la decisione. Fai rispettare il confine. Non moltiplicare le unità che devi governare. Metti l'isolamento dove la ricerca non può girare senza.

Sotto ognuno di questi c'è un controllo che non ho mai nominato, perché sembrava troppo ovvio da dire ad alta voce: una persona competente legge la modifica, la capisce e coglie cosa non va prima che vada in produzione. La review. Ogni pezzo dava per scontato, in silenzio, che qualcuno fosse ancora nel ciclo, a guardare.

Questo pezzo parla di cosa succede quando quell'assunto salta. Non perché qualcuno si sia impigrito, ma perché il codice inizia ad arrivare più in fretta di quanto un umano riesca a leggerlo e, peggio, smette di sembrare "veloce".

La prima metà di questa lezione l'ho imparata anni prima che qualcuno generasse codice da un prompt.

Il sistema che funzionava in ogni ambiente

Ho lavorato ad una piattaforma che inviava email su larga scala: campagne promozionali a tempo, e i normali messaggi transazionali che seguono un signup o un acquisto. Migliaia di contatti, alcuni invii schedulati con largo anticipo, altri scatenati da un'azione dell'utente.

Costruimmo lo scheduling su Google Cloud Scheduler, con un minimo di tracciamento nel database della piattaforma per sapere cosa era stato inviato. Era pulito. Lo Scheduler schedula le cose: è ciò che fa. Lo collegammo, tracciammo lo stato in locale, e lo guardammo funzionare.

E funzionava. In ogni ambiente prima della produzione, girava come un orologio. Dev, staging, tutto quanto. Ogni test che avevamo era verde.

Poi andammo in produzione, e l'impossibile cominciò a succedere tutto in una volta. Migliaia di email mai inviate. Migliaia inviate più e più volte, fino a dieci, alle stesse persone. Lo stato del job sul cloud andò fuori sincrono rispetto allo stato nel nostro database, così nessuno dei due poteva dirti la verità su cosa fosse davvero accaduto. E il servizio di invio esterno cominciò a restituire 429 Too Many Requests più in fretta di quanto riuscissimo a leggerli.

Ecco come ce ne accorgemmo. Non da un alert, perché non ne avevamo predisposti. Ce ne accorgemmo, perché l'agenzia di marketing veniva sommersa sui propri canali social da persone furiose per il bombardamento di email. Quando finalmente lanciammo le count sul database, impallidimmo.

Sistemarlo volle dire cambiare lo strumento sotto tutto quanto, mesi dopo che lo sviluppo era "finito": via lo Scheduler e dentro Google Cloud Tasks, una vera coda di task con le semantiche di consegna che avremmo dovuto progettare dal primo giorno, e, cosa altrettanto importante, un'unica autorità sullo stato di un job invece di due copie che divergono.

"Funzionava" non è mai stato "è corretto"

Nessuno fu negligente, qui. La scelta sembrava giusta. Uno scheduler per schedulare le cose è una frase plausibile, e plausibile è esattamente il problema.

Perché "funzionava in ogni ambiente" e "è corretto" non sono la stessa affermazione, e il divario tra le due è dove vivono i guasti costosi. I nostri test provavano che l'happy path funzionava. Non potevano mai provare l'assenza del guasto che ci fece davvero male, perché quel guasto non era nella logica del codice. Era in ciò che la produzione avrebbe fatto al codice: concorrenza reale, retry che scattano sui timeout, uno scheduler che garantisce che un job giri ma non che giri esattamente una volta, un servizio esterno con rate limit che incontravamo solo al volume reale.

Niente di tutto questo è visibile quando leggi il diff. La review guarda cosa il codice dice che farà. È quasi cieca a cosa gli farà il mondo una volta che il traffico sarà reale. Una suite di test che non simula mai il doppio invio, il timeout, la tempesta di retry, è una suite che resterà verde fino al momento esatto in cui la count dei clienti sul database ti farà stare male.

Codice "veloce" ne abbiamo sempre scritto. Solo che si vedeva

Ecco la parte che è cambiata, e non è quella di cui tutti si preoccupano.

Codice "veloce" ne abbiamo sempre scritto. Codice buttato giù sotto una deadline, codice scritto solo "per far funzionare" al momento. Non è una novità e non è il nemico. La vecchia valvola di sicurezza era che il codice "veloce" si annunciava da sé, e dove non lo faceva, lo annunciavamo noi. Sembrava fatto di corsa, quindi lo trattavamo come tale: lo mettevamo in quarantena, aprivamo un ticket, ce ne diffidavamo a vista, e lasciavamo un // FIXME o un // TODO nel codice stesso, un tuo appunto per il prossimo lettore che diceva non fidarti di questo, ancora. La bruttezza era informazione, e lo era anche il marcatore che mettevamo a mano.

La generazione a velocità di macchina toglie quel segnale. Il codice che un assistente produce in quantità è idiomatico, coerente, strutturato in modo plausibile, cosparso di test. Non indossa l'etichetta di avvertimento, e non si scrive da solo un FIXME. Un assistente non segnala la propria scorciatoia, perché da dentro non ha idea di averne presa una. Il mio errore con lo Scheduler almeno sembrava una decisione presa da qualcuno in un pomeriggio. La stessa classe di errore, generata con scioltezza e in massa, sembra codice di produzione già passato da una mano attenta.

È questa la svolta. Non che le macchine scrivano codice peggiore, ma che scrivono codice la cui "velocità" non si vede più. L'unica euristica su cui contavamo tutti, "diffida di ciò che sembra fatto di fretta", ha smesso in silenzio di funzionare, perché niente sembra più fatto di fretta.

Quando la review diventa teatro

Ora metti insieme le due metà.

La review era già l'anello più debole della catena: un umano, che leggeva, scommettendo che capire il codice fosse lo stesso che sapere cosa gli avrebbe fatto la produzione. Reggeva solo perché era all'incirca abbastanza veloce da tenere il passo con quanto in fretta producevamo il codice, e perché il codice che più aveva bisogno di sospetto di solito sembrava sospetto.

La produzione a velocità di macchina rompe entrambe le stampelle in un colpo. Non puoi leggere mille modifiche plausibili al giorno come avrei potuto leggere una singola decisione Scheduler-contro-Coda. E non puoi più lasciare che l'aspetto smisti la tua attenzione, perché l'aspetto è stato disaccoppiato dalla cura. Tieni la review come cancello in quelle condizioni e non diventa più attenta. Diventa teatro: un rito che produce la sensazione di aver controllato, nel momento esatto in cui ha smesso di poterlo fare.

Voglio essere preciso sulla mia posizione, perché è facile leggere tutto questo come paura degli strumenti. Non sono preoccupato per il codice che un assistente scrive per me. Lo uso, e funziona, per una ragione che è tutto il punto di questo pezzo: non ho mai considerato la review la cosa che tiene corretto un sistema. Sono preoccupato per qualunque sistema che lasci la review umana come ultima barriera tra un errore plausibile e la produzione, perché quella barriera era già sottile, e la velocità di macchina sta per scaricarci sopra tutto il suo peso.

Il controllo che scala

Se non puoi uscirne a forza di review, cosa resta?

Il controllo che tiene il passo di un output a velocità di macchina è l'enforcement a velocità di macchina. Non una persona che spera di accorgersene, ma la garanzia costruita dentro il sistema in modo che il guasto sia impossibile invece che improbabile.

Per la piattaforma email, non fu mai una review migliore del codice dello Scheduler. Fu scegliere una primitiva con le semantiche di consegna giuste, e far reggere l'invariante nell'architettura: dai a ogni messaggio una deduplication key, così lo stesso invio può scattare due volte e uscire una sola, e lascia che la coda possieda lo stato di un job, così non c'è una seconda copia in un database che possa andare fuori sincrono con esso. Un retry diventa sicuro per costruzione invece che per fortuna; la verità su cosa è girato vive in un posto solo invece che in due che si contraddicono. Fu un test scritto non per provare l'happy path ma per forzare il guasto: riesegui lo stesso trigger e fai fallire la build se un secondo messaggio sfugge. E, banalmente, fu l'alert che non avevo costruito: quello che urla quando gli invii-per-destinatario superano una soglia, così la count sul database mi raggiunge prima dei clienti furiosi.

È la stessa mossa di una fitness function che presidia una regola architetturale, o della row-level security che presidia un confine di tenant. In ogni caso la correttezza smette di dipendere da qualcuno che ricorda, capisce o coglie, e diventa una proprietà che il sistema fa rispettare da sé, ogni volta, alla velocità con cui il codice viene prodotto.

E la review non svanisce. Il giudizio umano si sposta a monte, verso le domande che il volume non può annegare. Non "questo codice è corretto?", che è senza risposta su mille modifiche al giorno, ma "cosa deve essere vero qualunque cosa faccia tutto questo codice?" e "quali guasti mi rifiuto semplicemente di far rilasciare?". È lì che un umano è insostituibile, ed è un uso molto migliore dell'unica risorsa scarsa e non scalabile dell'azienda che non leggere diff più in fretta.

La stessa mossa, ogni volta

Se ti sembra il resto della serie, è giusto così. È la stessa mossa, fatta un'ultima volta.

Scrivi la decisione, così la correttezza non dipende dalla memoria. Fai rispettare il confine, così non dipende dalla disciplina. Tieni separate le responsabilità senza renderle autonome, così non dipende dal coordinamento. Metti l'isolamento nell'indice, così non dipende dal fatto che ogni query se lo ricordi. Ognuna di queste toglie una garanzia dalla testa di un umano e la mette in un posto che regge da sé.

Questo pezzo si limita a dare un nome all'umano da cui quei quattro proteggevano in silenzio il sistema: quello alla tastiera, che legge, che fa onestamente del suo meglio, e a cui sta per essere chiesto di tenere il passo di una macchina.

L'unica cosa che non puoi spingere dentro la macchina

Una precisazione, perché tutto il pezzo pende con forza in una sola direzione. Il giudizio che ho appena spostato a monte, decidere cosa deve reggere e quali guasti ti rifiuti di rilasciare, è l'unica cosa che non devi provare a sistematizzare a tua volta. Trasforma quella comprensione in una checklist con punteggio o in un template di status e le avrai fatto esattamente ciò che la velocità di macchina ha fatto alla review: costruito uno snapshot che la gente impara a recitare. Una misura del giudizio diventa un obiettivo, e un obiettivo smette di essere giudizio. La ragione per cui quella comprensione non si può ingannare è che non è mai stata uno strumento.

Quindi la linea non è "imponi tutto". Imponi gli invarianti, perché una macchina li tiene meglio di quanto una persona potrebbe mai fare. Proteggi il giudizio su quali invarianti contino, perché una macchina non lo tiene affatto. L'abilità sta nel riconoscere in quale delle due categorie ricade ogni cosa, e metterla dal lato giusto di quella linea.

La regola che ti lascio

La velocità di macchina non ha creato questo problema. Ha tolto l'alibi. La review umana è sempre stata l'anello più debole, e l'ultimo che qualcuno volesse ammettere debole, perché era abbastanza veloce da tenere il passo e perché il codice pericoloso sembrava pericoloso. Entrambe queste scuse sono sparite.

Quindi smetti di chiedere ai tuoi reviewer di andare più veloce. Chiediti invece quali delle garanzie che ora tengono in testa potrebbero essere spostate nella build, nel type system, nel database, nel test che prova a rompere la cosa di cui hai paura. Tutto ciò che sposti lì è una garanzia che continua a reggere alle tre di notte, al millesimo commit, molto dopo che l'ultimo umano ha smesso di riuscire a leggerli tutti.

Dove, nel tuo sistema, l'attenzione di una persona è ancora l'unica cosa tra un errore plausibile e la produzione?