Articoli approfonditi sulla tecnologia che plasma il futuro.

Quando Git cede: lezioni da 30.000 merge al giorno

Come scalano (e come falliscono) i monorepo Git con merge estremi: lezioni da client sempre online e commit lato server.

Illustrazione di corrieri robot che affollano un banco di cassa in un vasto archivio di scatole di file.
Git presuppone che ogni client porti con sé l'intero archivio. Va bene, finché non arrivano migliaia di client al secondo.

Se gestisci un monorepo git, prima o poi arriverà un numero che farà saltare tutto. Per Anthropic quel numero era di circa 30.000 merge al giorno, una cronologia del branch principale da 100 GB e circa 25 milioni di checkout CI ogni giorno, che, recuperati in modo ingenuo, significano oltre 600 petabyte di traffico git. Ogni singolo giorno. La loro risposta è stata scrivere un proprio sistema di controllo di versione in cinque mesi. La tua non dovrebbe essere questa. Ma gli errori in cui sono incappati sono gli stessi che vedo fare ai team a un decimo di quella scala, quindi vediamoli uno per uno.

Errore numero uno: trattare un flusso di lavoro centralizzato come se fosse distribuito

La scommessa di fondo di git è la decentralizzazione. Ogni clone è un repository completo: cronologia completa, object store completo, pienamente funzionante offline. Questa scommessa ha portato git al successo mondiale e, per la maggior parte dei repo, è ancora quella giusta. Ma c'è una cosa che quasi nessuno ammette: quasi nessuno usa davvero git in questo modo. La tua organizzazione ha un unico remote autorevole su GitHub o GitLab. Gli sviluppatori fanno push e pull di continuo. I tuoi runner CI sono, per definizione, sempre online. Stai usando un sistema centralizzato travestito da distribuito, e ne paghi i costi: ogni job CI che scarica una cronologia che non leggerà mai, ogni sandbox di agenti che materializza packfile di cui non ha bisogno.

Questa è la prima vera lezione dal ridisegno di Anthropic. Quando hanno fatto un passo indietro, hanno notato che i loro client erano «agenti e job che sono online per definizione», quindi hanno progettato attorno a un server sempre raggiungibile. Il client è stato reso volutamente semplice: una cache locale limitata, una working copy su disco, un filesystem watcher. È il server a rispondere a blame, calcolare diff ed eseguire merge. È la stessa conclusione a cui sono arrivati Google con Piper e Meta con Sapling, e non è una posizione anti-git: è il riconoscimento che l'architettura dovrebbe corrispondere al carico di lavoro reale, non a quello per cui lo strumento è stato pensato vent'anni fa. Ho visto questo schema anche nei tool di sicurezza: i team distribuiscono un'architettura di agenti con capacità offline, poi la eseguono in un ambiente dove l'offline non esiste mai, e si chiedono perché il loro livello di sincronizzazione sia un incubo.

La conclusione per il tuo team non è «costruite un VCS». È: verificate le vostre assunzioni. Scrivete quali proprietà di git usate davvero. Lavoro offline? Fork reali? Firma crittografica dei commit? Se la risposta onesta è «un solo remote, sempre online, la CI fa tutto», smettete di ottimizzare per un flusso distribuito che non avete e iniziate a ottimizzare i costi di checkout e di query.

Errore numero due: nascondere la scala con mirror e seed

Ogni team che lavora con repo di grandi dimensioni attraversa le stesse fasi di frustrazione. Prima i clone shallow. Poi i checkout sparse e i promisor remote. Poi i mirror git interni, e infine i «seed» di snapshot nello storage a oggetti, così i job CI possono partire da qualcosa di vicino alla punta del main. Anthropic ha usato tutte queste soluzioni, arrivando al picco a diverse centinaia di mirror interni. E la modalità di guasto è sempre la stessa: le soluzioni alternative non falliscono rumorosamente, marciscono. I seed si allontanano sempre di più dalla punta tra un refresh e l'altro. I clone parziali rompono git blame e git log --follow in modi confusi. Il bisect di qualcuno restituisce silenziosamente risultati sbagliati perché la cronologia è troncata. Nessuno se ne accorge finché non c'è una review dopo un incidente.

Quest'ultimo punto è quello che mi tiene sveglio la notte. I bug di correttezza sulla cronologia parziale sono esattamente il tipo di errore sottile che vive nello scarto tra ciò che pensi garantisca il sistema e ciò che fa davvero. Un clone shallow non dà errore quando chiedi il blame oltre la sua profondità; ti restituisce semplicemente una risposta parziale. Se la tua pipeline di revert automatico si fida di quella risposta, hai costruito una macchina che annulla con sicurezza la modifica sbagliata.

  • I clone shallow rompono gli strumenti che dipendono dalla cronologia (blame, bisect, merge-base) in modi che falliscono in silenzio, non rumorosamente.
  • I seed di snapshot si allontanano dalla punta del main; più alto è il tasso di merge, più veloce è la deriva, quindi la soluzione alternativa peggiora proprio quando il carico aumenta.
  • Le flotte di mirror moltiplicano la superficie operativa: centinaia di copie stateful che possono ritardare, corrompersi o esaurire il disco in modo indipendente.
  • Ogni soluzione alternativa che tocca la semantica della cronologia richiede un test di regressione, altrimenti ti morderà durante un incidente.

La correzione alla scala di Anthropic è stata strutturale: non esiste nessuna copia completamente materializzata del repo da nessuna parte, nemmeno sul server, e tutto è indirizzato per contenuto e cacheabile orizzontalmente. Alla tua scala la correzione è più modesta: misura quante macchine contengono copie complete o quasi complete del repo, e sii onesto sulla deriva e sulle modalità di guasto di ciascuna.

Errore numero tre: far mantenere a ogni job CI una propria copia del repo

Vale la pena chiedersi quanti dei job della tua flotta CI abbiano davvero bisogno di una working copy. La risposta di Anthropic, dopo aver costruito il loro sistema, è stata «molti meno di quanti pensavamo». Un job che ha bisogno del diff tra un commit e il suo merge base, del blame di un file o dell'elenco dei commit che toccano un percorso, può chiederlo direttamente al server tramite API. Niente clone, niente fetch, niente disco. Le loro pipeline automatiche di bisect e revert «non sarebbero pratiche su scala» senza quell'accesso programmatico economico, e ci credo, perché ho visto team provare a far girare le stesse pipeline sopra clone locali e affogare nei tempi di fetch.

Questo principio vale ben oltre il controllo di versione. Tratta il checkout come l'eccezione costosa, non come il default. Quando un job ha davvero bisogno dei file, i checkout sparse e virtualizzati, che materializzano solo le directory richieste o addirittura solo i file effettivamente letti, riducono i costi di ordini di grandezza. Il protocollo di fetch di Anthropic ha eliminato del tutto la negoziazione tipica di git: il client richiede alberi e blob immutabili specifici, qualunque cache può rispondere, e il sistema si comporta come una CDN a più livelli invece che come un server intelligente che calcola packfile per ogni client. Se hai mai profilato un server git che va in crisi sotto il carico della CI, riconoscerai perché conta: la generazione dei packfile è pesante in CPU proprio perché il protocollo chiede al server di calcolare un delta rispetto a ciò che ogni client ha già.

Se i tuoi client sono sempre online e c'è un'unica copia autorevole, ogni clone locale che mantieni è una cache per cui paghi il prezzo pieno e da cui ottieni comunque la semantica di una cache.

Errore numero quattro: lasciare che il costo del commit cresca con la dimensione del repo

La decisione ingegneristica più interessante del loro design è il changeset. Un commit git punta a un albero che rappresenta l'intero stato del repository; il loro changeset registra solo la lista dei file modificati rispetto al primo genitore, più puntatori indirizzati per contenuto nello storage dei blob, aggiunti a un write-ahead log. Niente packfile da accettare, nessun locking, nessun coordinamento tra server. I ref, cioè branch e tag, sono deliberatamente messi in secondo piano perché «uno stato globale su cui ogni server deve concordare è un problema di scalabilità in agguato». Spostare un ref è un compare-and-swap su una singola riga di Spanner, disaccoppiato dalla creazione del commit.

Illustrazione stilizzata di pacchi su un nastro trasportatore che alimenta un log luminoso e strutture dati derivate.
Registra solo ciò che è cambiato in un write-ahead log; ricava tutto il resto (blame, storia dei percorsi, indici del grafo) in modo pigro a partire da esso.

L'idea complementare è quella dei «dati derivati»: indici di blame, storia dei percorsi, skip list del commit-graph, tutti calcolati ricorsivamente dai genitori di ogni changeset, attraverso un unico meccanismo generico. Si calcolano in modo eager o lazy a seconda del tipo. Si aggiungono nuovi tipi di indice senza toccare il client. È un pattern davvero valido per qualsiasi sistema event-sourced: ho consigliato la stessa forma per le pipeline dei log di audit, dove il log append-only è la verità e ogni struttura di query è una proiezione ricostruibile. Nel momento in cui qualcuno mantiene a mano una proiezione al di fuori del meccanismo, il disallineamento è garantito.

Una precisazione dal punto di vista della sicurezza: chi si preoccupa della centralizzazione ha un punto che merita di essere preso sul serio. Un unico sistema lato server dove vivono commit, merge e query sulla cronologia è un bersaglio di alto valore, e agenti AI con permessi di scrittura su «l'unico repo che governa tutti» concentrano male il rischio. Anthropic ha costruito in parallelo un «rich source control IAM system», e i dettagli contano più delle strutture dati. Se centralizzi, il calcolo del raggio d'azione di un incidente cambia: prevedi isolamento, credenziali per singolo agente e audit fin dal primo giorno, non al terzo trimestre. I team che fanno girare agenti stateful su infrastrutture condivise conoscono già questo problema.

Errore numero cinque: pensare che 30.000 merge al giorno significhino 30.000 buone decisioni al giorno

L'ingegneria è impressionante. La domanda aperta riguarda ciò che passa attraverso di essa. Un professionista lo ha detto senza giri di parole: ogni volta che smette di rivedere il codice generato dall'AI, «poi trovo roba scadente. Non ogni review trova roba scadente, ma abbastanza spesso da non potermi permettere di smettere di rivedere tutto». Moltiplica questo tasso di errore per 30.000 merge al giorno e capirai perché Anthropic ha dovuto costruire anche «modi nuovi di gestire e far atterrare grandi quantità di codice revisionato»: la superficie di review, la coda di approdo, le pipeline di revert automatico. Il VCS non era il prodotto. Il prodotto era la pipeline di approdo. Il controllo di versione è diventato il collo di bottiglia solo dopo che la capacità di review lo era diventata a monte.

C'è anche un dettaglio sincero nascosto nei loro racconti: gli ingegneri del progetto hanno descritto gran parte del loro ruolo come dire all'agente di programmazione «no». La pipeline di ingestion prototipale scritta a mano andava bene, finché un modello più recente non l'ha riscritta con un consumo di memoria 20 volte inferiore e 4 volte più veloce, ma per arrivarci servivano persone con gusto che decidessero cosa accettare. Corrisponde a tutto ciò che ho visto: l'AI sposta il lavoro dell'ingegnere dalla scrittura alla revisione, e il throughput di review è ormai la risorsa scarsa. Abbiamo già scritto di come ogni livello di review rallenti un team: il problema è che, con codice generato da agenti, eliminare livelli di review non è sicuro. Servono strumenti di review migliori, non meno review.

Cosa dovresti fare davvero

Tu non sei Anthropic. Non hai il loro budget di token, come loro stessi ammettono, e non hai 25 milioni di checkout al giorno. Ma gli strumenti di coding AI stanno spingendo verso l'alto il tasso di merge di tutti, e i punti di pressione arrivano in un ordine prevedibile. Ecco l'ordine delle operazioni che seguirei, secondo la Legge del minor rimpianto di Priya:

  1. Misura prima di tutto. Conta i checkout giornalieri, i byte scaricati per ogni job CI e quante macchine contengono copie complete del repo. Probabilmente hai un problema di spreco prima ancora di un problema di scala.
  2. Elimina i checkout non necessari. Sposta le domande sui metadati (diff, blame, antenati) verso le API (GitHub/GitLab o un servizio interno leggero) prima di comprare qualsiasi infrastruttura.
  3. Rendi sicuri i clone sparse e parziali. Testa esplicitamente i tuoi strumenti di bisect, blame e revert sui clone shallow, oppure vietali nell'automazione.
  4. Imposta come default per la CI i fetch lato server di oggetti immutabili dove possibile, e tratta le flotte di mirror come debito tecnico con un tasso di interesse composto.
  5. Investi nella pipeline di approdo, non nel formato del repo. Capacità di review, merge queue e revert automatico sono dove lo sviluppo su scala agentica si rompe davvero per primo.
  6. Solo a quel punto valuta architetture VCS alternative: Sapling è open source e ti dà la maggior parte dei benefici client-server senza dover scrivere il tuo.

La lezione più profonda qui non riguarda affatto git. È che ogni strumento incorpora assunzioni sul proprio carico di lavoro (esseri umani offline, un commit alla volta, codice scritto più lentamente di quanto venga rivisto), e quelle assunzioni vengono violate proprio ora dagli agenti, nell'infrastruttura, nella review e nei confini di sicurezza. I team che ne usciranno meglio non saranno quelli che ricostruiranno tutto da zero. Saranno quelli che avranno notato quali assunzioni si sono rotte per prime, e avranno sistemato esattamente quelle.