Il versioning protegge solo ciò che volevi pubblicare

Anni fa ho dovuto integrare una piattaforma di monitoraggio centralizzata con il sistema di allarme di uno dei vari vendor che supportavamo. Il vendor aveva un manuale tecnico per le terze parti. Contava centinaia di pagine in PDF, e ben poco era scritto per un essere umano.

L'handshake di autenticazione era documentato. Quello che succedeva dopo no. Da nessuna parte il manuale diceva come i pacchetti TCP successivi andassero legati alla sessione appena autenticata. L'algoritmo di cifratura era descritto in una notazione che non era né pseudo-codice né un linguaggio che conoscessi, senza una sola spiegazione a parole di cosa dovesse fare.

Dopo settimane senza venirne a capo, ho preteso di essere messo in contatto direttamente con il vendor. Quasi un mese dopo è arrivata la risposta. Era la foto di un post-it, scritto a penna da uno dei loro sviluppatori. Valeva esattamente quanto valgono di solito i post-it.

Alla fine l'ho fatto funzionare. Non grazie alla documentazione, non grazie all'interfaccia pubblica, non grazie al post-it. L'ho fatto funzionare alla vecchia maniera: ipotesi, catture del traffico di rete, e una lunga serie di tentativi finché l'altra parte non ha smesso di rifiutarmi. Ci sono voluti sei mesi, per qualcosa che un manuale coerente avrebbe ridotto a poche settimane.

Una volta consegnata, l'integrazione non si è mai rotta, e sono passati diversi anni. Ne sono stato orgoglioso a lungo. Mi ci è voluta un'eternità per capire che cosa avessi davvero costruito.

Ogni byte di quell'integrazione poggiava su comportamenti che il vendor non aveva mai promesso. Il modo in cui le sessioni erano davvero legate, la forma esatta della cifratura così come si comportava in rete, l'ordine in cui il dispositivo si aspettava le cose. Niente di tutto questo era nel contratto, perché il contratto non lo descriveva. Se il vendor avesse cambiato una qualunque di queste cose, restando del tutto dentro la propria documentazione, la mia parte si sarebbe rotta. E loro non avrebbero avuto alcun modo di sapere che io c'ero.

Il fatto che abbia retto per anni è meno rassicurante di quanto sembri. Significa che il vendor non ha mai toccato le parti da cui dipendo, e io non ho modo di sapere se sia stata una decisione o fortuna.

Il contratto è tutto ciò che possono osservare

Questo fenomeno ha un nome, e viene da un ingegnere di Google, Hyrum Wright. Con un numero sufficiente di utenti di un'API, non importa cosa prometti nel contratto: tutti i comportamenti osservabili del tuo sistema diventeranno una dipendenza per qualcuno.

Quasi tutti i team la sentono come una curiosità. Io penso che sia il fatto più sottovalutato del lavoro di integrazione.

Ecco come si presenta in pratica. L'ordine in cui capita che arrivino i webhook, su cui la riconciliazione di un partner fa affidamento senza dirlo. Un campo che lo schema segna come opzionale ma che è presente in ogni risposta da tre anni. La dimensione di pagina di default. Il tempo che un'operazione di solito impiega, contro cui qualcuno ha tarato il proprio timeout. Il testo esatto di un messaggio d'errore, che qualcuno analizza con un'espressione regolare perché non c'era un codice d'errore da leggere. Il fatto che gli ID siano crescenti.

Nessuna di queste cose è stata pubblicata. Tutte fanno ormai parte del contratto, perché il contratto che conta è quello su cui i tuoi consumer hanno davvero costruito, e loro hanno costruito su ciò che potevano vedere.

Il versioning non ci arriva

La risposta standard ai breaking change è il versioning. Pubblichi la v2, tieni in piedi la v1, dai alla gente una finestra di migrazione. Funziona, per ciò che copre.

Ciò che copre è il comportamento che intendevi pubblicare. Il versioning è una promessa sulla specifica: non cambieremo ciò che ti abbiamo detto senza avvisarti. Non dice niente sul comportamento di cui non hai mai parlato a nessuno, perché dal punto di vista del produttore quel comportamento non fa nemmeno parte dell'API. È un dettaglio implementativo. Lo cambi in una patch, tutti i contract test che possiedi restano verdi, il changelog è onesto, e un partner va giù di martedì.

È questa la parte scomoda. Nessuno ha sbagliato niente. Il produttore ha mantenuto ogni promessa. Il consumer ha costruito sull'unica cosa che aveva a disposizione. La rottura è avvenuta nello spazio tra le due, ed è proprio lo spazio dove nessun numero di versione arriva.

Il produttore è l'ultimo a saperlo

Il secondo problema è la visibilità. Quel vendor non aveva idea di cosa avessi ricostruito per tentativi. E come lui quasi tutti i produttori.

I dati d'uso ti dicono quali endpoint vengono chiamati e quanto spesso. Non ti dicono su quali comportamenti si fa affidamento. Puoi vedere che un partner chiama l'endpoint degli eventi un milione di volte al giorno. Non puoi vedere che la sua riconciliazione dà per scontato che gli eventi arrivino in ordine, perché quell'assunzione vive nel suo codice, scritta da qualcuno che nel frattempo ha cambiato team, e magari documentata su un post-it.

Così la visione che il produttore ha del proprio contratto è sistematicamente più piccola di quello reale. Vede la specifica, e la specifica è proprio la parte che non causa mai l'incidente.

Il tuo costo di cambiamento è il costo di uscita del tuo partner

Due settimane fa ho scritto del costo di uscita da un fornitore: il prezzo di andarsene, che si accresce dopo la firma e non compare mai in fattura. Questo è lo stesso costo visto dall'altro lato del confine.

Ogni comportamento non promesso su cui un consumer costruisce alza due numeri insieme. Alza il costo che il consumer pagherà per lasciarti, perché tutto quell'adattamento andrà ricostruito altrove. E alza il tuo costo per cambiare qualunque cosa, perché adesso c'è una dipendenza che non vedi e che non puoi versionare. È un solo numero con due proprietari, e nessuno dei due l'ha messo per iscritto.

Ed è anche il motivo per cui "facciamo la v2" in pratica tende a bloccarsi. La finestra di migrazione si chiude, metà dei partner non si è spostata, e il motivo per cui non si è spostata raramente sono le differenze documentate. Sono quelle non documentate: i comportamenti della v1 su cui hanno costruito senza saperlo, e che la v2 non riproduce.

Cosa può fare il produttore

La legge di Hyrum non la puoi far sparire. Puoi decidere da quali comportamenti sei disposto a lasciar dipendere gli altri, e rendere gli altri più difficili da usare per sbaglio.

Prometti apposta, o rifiuta apposta. Se un ordinamento è garantito, documentalo e testalo. Se non lo è, rendilo visibile. L'esempio più pulito che conosco è nel linguaggio Go, che randomizza di proposito l'ordine di iterazione delle mappe, così nessuno può arrivare a dipendere da un ordine mai promesso. La stessa mossa funziona per le API: mescola ciò che non vuoi promettere, fai variare ciò che non vuoi congelare, restituisci codici d'errore così nessuno deve analizzare la tua prosa.

Chiedi ai consumer su cosa fanno affidamento. I contract test consumer-driven ribaltano la direzione del contratto: ogni consumer dichiara, in un test che gira nella tua pipeline, il comportamento da cui dipende davvero. Non intercetta tutto, perché i consumer non conoscono tutte le proprie assunzioni. Ne intercetta molte di più della tua specifica, e le intercetta prima del rilascio invece che dopo.

Rompilo apposta prima che lo faccia la produzione. Un lettore del pezzo della settimana scorsa mi ha dato una regola a cui non ho più smesso di pensare: un controllo che non hai mai visto diventare rosso è un'ipotesi, non un controllo. La versione per le API è una sandbox o un canale canary dove il comportamento non promesso cambia di proposito. Mescola l'ordine, togli il campo opzionale, rallenta la risposta, riformula l'errore. I partner che si rompono lì ti hanno appena detto, a basso costo, qual è il tuo contratto reale.

Aggiungi una riga alla review dell'API. Non solo quali comportamenti abbiamo pubblicato, ma anche: quali comportamenti vengono consumati senza che li abbiamo mai promessi? Nessuno può rispondere per intero. Fare la domanda è il modo in cui la risposta comincia a esistere.

Cosa può fare il consumer

L'altra metà è mia, ed è la lezione che ci ho messo più tempo a imparare da quel gateway di allarmi.

Quando costruisci su un comportamento che nessuno ti ha promesso, scrivilo esattamente per quello che è. Non come documentazione del sistema del vendor, che è compito loro, ma come elenco delle tue assunzioni: facciamo affidamento sul fatto che le sessioni siano legate così, che i pacchetti arrivino in quest'ordine, che questo campo ci sia sempre. Poi fissa ciascuna con un test che eserciti il comportamento reale, così il giorno in cui cambia lo scopri dalla tua pipeline invece che da un incidente.

All'epoca non l'ho fatto. Tutto ciò che avevo imparato per tentativi viveva nella mia testa e nel codice, e da nessun'altra parte. Se dovessi rifarlo, quell'elenco sarebbe l'artefatto più prezioso dell'intera integrazione. È la differenza tra dipendere da qualcosa e sapere di dipenderne.

C'è un tetto

L'argomento si può spingere troppo in là, quindi segno dove si ferma.

Se ogni comportamento osservabile fa parte del contratto, allora ogni cambiamento rompe qualcuno, e preso alla lettera significa che non puoi più cambiare niente. Non è questa la conclusione. Il comportamento non promesso lo cambierai, continuamente, ed è giusto farlo. Il punto è farlo sapendo chi rischi di rompere, invece di scoprirlo dal suo ticket di supporto.

Congelare tutto è l'eccesso di opzionalità di qualche settimana fa con un badge da integrazione: un'interfaccia portata avanti per anni perché nessuno osa toccare le parti che nessuno vede. Lo scopo è più stretto. Rendi visibili i comportamenti non promessi che contano, rendi impossibile fare affidamento su quelli che non contano, e fai del momento del cambiamento una decisione con un nome, invece dell'effetto collaterale di una patch.

La regola che ti lascio

Il versioning protegge ciò che volevi pubblicare. I tuoi consumer dipendono da ciò che hai effettivamente pubblicato, cioè tutto ciò che potevano vedere.

Se produci un'interfaccia, la domanda non è se hai mantenuto le tue promesse. È se sai su quali comportamenti la gente fa affidamento senza che tu li abbia mai promessi. Se ne consumi una, la domanda è se hai scritto su cosa fai affidamento che nessuno ti ha promesso, o se vive, come viveva la mia, nella tua testa e sul post-it di qualcun altro.

Quindi ecco quella che farei sulle tue integrazioni. Prendi il partner che meno vorresti rompere. Da cosa dipende che tu non gli hai mai promesso, e come lo scopriresti prima di lui?