Il worker è morto. La sincronizzazione no.

Uccidi il worker a metà di un batch e segui un contatto: quale cursore è stato salvato, perché la scrittura ripetuta non arriva due volte, chi possiede la sincronizzazione mentre riparte. Poi i due incidenti che hanno mostrato che il ripristino era solo metà del lavoro.

ripristino sincronizzazione dopo errore

Che cosa succede a una sincronizzazione in corso quando il worker muore a metà di un batch?

Il cursore avanza solo alla fine di un batch senza errori, quindi un worker ucciso a metà di un batch lo lascia all’inizio di quel batch e l’esecuzione successiva legge la stessa pagina. La chiave di consegna di ogni record è derivata dal cursore di pagina e dalla sua posizione, e un registro di sette giorni delle scritture completate impedisce che la ripetizione arrivi due volte. Una sola concessione temporanea per sincronizzazione impedisce a due esecuzioni di lavorare insieme. Questa era la metà facile. Una sincronizzazione ha continuato a fallire, esecuzione dopo esecuzione, prima che qualcosa la mettesse in pausa, e un workspace è rimasto in silenzio, con errori e nessun successo, prima che qualcosa se ne accorgesse. Per questo ora c’è un interruttore che mette in pausa dopo cinque errori consecutivi e un allarme orario dopo ventiquattro ore senza un successo.

Scopri di più
In questa pagina

In un talk del team relazioni con gli sviluppatori di Crusoe, pubblicato il 3 ottobre 2026 da AI Engineer, è stato avviato un job di addestramento PyTorch su due nodi A100. Poi al sistema di monitoraggio della demo stessa è stato comunicato che una GPU aveva generato un errore XID 79. Il nodo è stato segnato come fuori servizio e il job annullato. Il processo ha ricevuto un SIGTERM e “fino a due minuti per gestire quel SIGTERM, per salvare i tuoi checkpoint o svuotare i tuoi log”. È subentrato un nodo di riserva e “il job slurm ripartirà. Caricherà il checkpoint e continuerà l’addestramento da dove si era fermato”. Poco meno di quindici minuti dall’inizio alla fine. Nessuno è stato chiamato in reperibilità.

Una sincronizzazione è un animale molto più piccolo di un addestramento. La domanda è la stessa. Di che cosa ha bisogno un’esecuzione perché uccidere il worker costi pochi minuti e nessun duplicato?

Questa è la risposta per il motore di sincronizzazione di Tajo, letta nel codice e non in una brochure. Il Reverse ETL descrive la forma del lavoro. Questo è ciò che succede quando il lavoro muore.

Segui un contatto

Un’esecuzione legge la sorgente una pagina alla volta, a partire da un cursore salvato. Chiamiamo c3 il cursore di pagina e il nostro contatto il 212º record di quella pagina. Prima che venga scritto qualsiasi cosa, per quel contatto valgono tre cose.

Primo, ha una chiave di consegna. La chiave è un hash dell’id della sincronizzazione, della sorgente, del cursore di pagina, della posizione nella pagina e del record stesso. Il cursore e il record entrano nell’hash e non vengono mai incorporati nella chiave, quindi la chiave non contiene alcun indirizzo email.

Secondo, la scrittura passa da un registro, non va direttamente alla destinazione. Prima di chiamare la destinazione, la chiave viene prenotata come in attesa. Quando la scrittura riesce, la riga viene segnata come completata e conservata per sette giorni.

Terzo, il cursore non si è mosso. Si muove solo alla fine di un batch, solo se ogni record di quel batch è arrivato e solo dopo che l’esecuzione ha dimostrato di possedere ancora la sincronizzazione.

Ora uccidi il worker tra la scrittura e il checkpoint.

Con un SIGTERM ogni worker della coda si svuota entro un’unica scadenza condivisa di quindici secondi, e il processo termina dopo venticinque secondi qualunque cosa sia ancora in esecuzione; systemd gliene concede quarantacinque prima di ucciderlo. Un’esecuzione che non è finita entro quella finestra resta segnata come in corso, e il worker successivo la riprende tramite il recupero dei job bloccati della coda. Con un’uccisione forzata non c’è alcuno svuotamento. In entrambi i casi valgono gli stessi due fatti. La destinazione ha il nostro contatto. Il nostro cursore dice ancora c3.

La ripetizione

L’esecuzione successiva parte da c3, legge la stessa pagina, deriva la stessa chiave per il 212º record e interroga il registro. Il registro risponde: completata. La destinazione non viene chiamata; il risultato memorizzato viene riprodotto e l’esecuzione prosegue. Sul percorso massivo, dove l’endpoint di importazione di Brevo riceve un’intera pagina in una sola chiamata, lo stesso controllo viene eseguito per ogni chiave prima di costruire la richiesta. Una pagina in cui ogni chiave è già completata non invia nulla.

È tutto qui il trucco, e vale la pena dire perché funziona. La chiave è derivata dal cursore di pagina all’inizio del batch, non dall’esecuzione che in quel momento era attiva. Un crash, un nuovo tentativo sessanta secondi dopo e un’esecuzione pianificata domani derivano tutti la stessa chiave per lo stesso record. Al registro non importa quale di loro stia chiedendo.

Ora il costo. Il registro dimentica dopo sette giorni. Il commento del codice stesso dice che sette giorni coprono un nuovo tentativo ritardato e un breve disservizio, e non pretende altro. Una ripetizione da un cursore più vecchio di così scrive di nuovo. E un’esecuzione interrotta in modo forzato può dover aspettare. La concessione temporanea dell’esecuzione morta resta valida fino a quindici minuti, e in una sincronizzazione pianificata un’esecuzione la cui concessione temporanea è libera viene raccolta e ripianificata solo se è partita da più di un’ora. La demo di Crusoe riparte in meno di quindici minuti. La nostra non sempre.

Chi possiede la sincronizzazione mentre riparte

Una sola esecuzione alla volta possiede una sincronizzazione. L’esecuzione prende una concessione temporanea in Redis con il proprio token, una durata di quindici minuti e un rinnovo ogni cinque minuti. Prima di ogni checkpoint verifica di avere ancora quella concessione. Se non ce l’ha, si ferma. L’esecuzione viene registrata con la frase semplice che la persona leggerà: “The run lost its slot part-way through and stopped to avoid writing twice.” (L’esecuzione ha perso il suo posto a metà strada e si è fermata per evitare di scrivere due volte). Una seconda esecuzione che si scontra con un proprietario attivo non viene ritentata. Termina con “Another run of this sync was already in progress” (Un’altra esecuzione di questa sincronizzazione era già in corso), e l’esecuzione pianificata successiva parte normalmente.

Anche la responsabilità della correzione è scritta. Ogni codice di errore corrisponde a una frase su che cosa è successo, una su che cosa fare dopo e un responsabile: tu o Tajo. Una destinazione che ha rifiutato ogni record perché la mappatura è sbagliata è compito tuo. Una concessione temporanea persa è compito nostro. La pagina dell’esecuzione mostra il passo successivo; la guida al ripristino della sincronizzazione indica quale dei due.

Fin qui la metà facile. Un crash costa una ripetizione e nulla arriva due volte. Due incidenti ci hanno insegnato l’altra metà.

Errore dopo errore

Una sincronizzazione ha fallito più e più volte. Non in un ciclo serrato: una volta per intervallo dello scheduler, esecuzione dopo esecuzione, con lo stesso messaggio ogni volta, cioè che un id personalizzato non può contenere i due punti. L’esecuzione non ha mai raggiunto la destinazione. Moriva mentre veniva messa in coda, prima che un esecutore la vedesse, quindi un allarme basato sull’esecutore non ha mai avuto nulla su cui basarsi.

Niente sorvegliava la serie. Questa è tutta la scoperta.

La correzione è un interruttore. Dopo ogni errore registrato legge le ultime cinque esecuzioni concluse della sincronizzazione dall’ultima volta in cui è stata modificata. Se tutte e cinque sono errori, mette in pausa la sincronizzazione, scrive una riga di audit con il motivo circuit_breaker_tripped e lancia un allarme critico. La serie è derivata dalle esecuzioni memorizzate, non da un contatore nel worker. Conta un errore registrato da qualsiasi percorso, compreso l’errore di messa in coda che ha causato l’incidente. Il commento del codice dice che avrebbe fermato quell’incidente alla quinta esecuzione. Un solo successo azzera la serie. Togliere la pausa riarma l’interruttore invece di farlo scattare di nuovo sulle vecchie righe. La guida al connettore Shopify dice la stessa cosa in una riga: dopo cinque esecuzioni fallite consecutive la sincronizzazione si mette in pausa da sola e la sua pagina mostra l’ultimo errore.

Ciò che l’interruttore non fa è riparare qualcosa. Crusoe sostituisce il nodo e riavvia il job senza chiedere. Tajo si ferma e chiede: una sincronizzazione in pausa resta in pausa finché una persona non corregge la causa e non la pubblica di nuovo. Questa impostazione prudente è voluta: un nuovo tentativo automatico di una sincronizzazione che scrive su record di clienti è esso stesso una scrittura.

Il silenzio che nessuno ha visto

L’obiezione è legittima, e suona così: “Un interruttore che mette in pausa dopo cinque errori avrebbe intercettato anche il secondo incidente. Perché costruire un secondo sorvegliante?”. Perché il secondo incidente non sembrava un errore. Per un lungo periodo un workspace ha avuto una sincronizzazione attiva, una pila crescente di righe di errore e nemmeno un successo, mai, e nulla ha lanciato un allarme. Ogni segnale esistente si basava su un’attività: un’esecuzione è fallita, è arrivato un webhook, è stato superato un budget. Zero successi non è un evento. È l’assenza di un evento, e un interruttore entra in azione solo quando viene registrato un errore. Uno scheduler morto, una coda bloccata, un worker che non si avvia mai: niente di tutto questo registra qualcosa, e un interruttore non si sveglia mai.

Il dispositivo a uomo morto di una locomotiva funziona al contrario. Il macchinista deve continuare a premerlo, e il silenzio ferma il treno. La versione per le sincronizzazioni gira una volta all’ora, al tredicesimo minuto. Per ogni workspace con una sincronizzazione attiva pone una sola domanda: qualche esecuzione è riuscita nelle ultime ventiquattro ore? Un workspace la cui sincronizzazione attiva più vecchia è stata abilitata meno di un giorno fa viene lasciato in pace; non ha ancora avuto un giorno per riuscire. Uno più vecchio e silenzioso riceve un allarme critico. L’allarme dice di quale silenzio si tratta: esecuzioni registrate ma nessuna riuscita, oppure nessuna esecuzione registrata. Scatta una volta per incidente. Un marcatore in Redis lo tiene chiuso fino alla prima esecuzione riuscita, poi si cancella, così l’incidente successivo genera di nuovo un allarme.

I due sorveglianti sono arrivati in un unico commit, a posteriori. Il commento dell’interruttore dice che avrebbe fermato il primo incidente alla quinta esecuzione. Quello dell’uomo morto dice che una sincronizzazione abilitata molto tempo fa con zero esecuzioni deve generare un allarme. Nessuno dei due esisteva quando serviva. Li abbiamo scritti dopo, con entrambi gli incidenti annotati nell’intestazione del file perché nessuno dimentichi il motivo.

Che cosa è costruito e che cosa no

Costruito, al momento in cui scriviamo:

  • Il cursore viene salvato alla fine di ogni batch senza errori, e di nuovo in caso di crash.
  • La chiave di consegna di ogni record è derivata dal cursore di pagina e dalla sua posizione, e confrontata con un registro di sette giorni prima di ogni chiamata alla destinazione.
  • Una concessione temporanea per sincronizzazione, con una durata di quindici minuti.
  • Cinque tentativi per esecuzione, con attese crescenti a partire da sessanta secondi.
  • Un interruttore che mette in pausa la sincronizzazione dopo cinque errori consecutivi e registra il motivo.
  • Un allarme orario dopo ventiquattro ore senza un successo.

Non costruito, e da sapere prima di farci affidamento:

  • L’uomo morto sorveglia il workspace, non la sincronizzazione. Una sincronizzazione sana nasconde una sorella silenziosa nello stesso workspace.
  • Il suo allarme raggiunge gli operatori di Tajo: sempre una riga di log, e Sentry, un webhook o un’email dove configurati. Non compare nella pagina della sincronizzazione. La pausa dell’interruttore sì.
  • Niente riavvia una sincronizzazione in pausa. Lo fa una persona.
  • Il ripristino dopo un’uccisione forzata non è veloce. La concessione temporanea può restare valida quindici minuti e la raccolta aspetta un’ora.
  • Entrambe le soglie, cinque errori e ventiquattro ore, sono impostazioni del deployment. Non puoi impostarle per singola sincronizzazione.

Una lunga catena di passaggi quasi perfetti fallisce comunque; la via d’uscita è definire lo stato finale e verificarlo. Questo articolo ne è la metà operativa. Definisci che cosa lascia dietro di sé un’esecuzione morta, e verifica il nulla che lascia il silenzio.

La frase di chiusura di Crusoe era che “i guasti sono inevitabili”. La nostra va un passo oltre. Il crash era la metà facile, perché lasciava delle righe. Il silenzio era la metà che siamo dovuti andare a cercare.

Domande frequenti

Che cosa succede a una sincronizzazione in corso quando il worker muore a metà di un batch?

Il cursore punta ancora all’inizio del batch interrotto, perché Tajo lo sposta solo dopo che un batch è arrivato senza errori. L’esecuzione successiva rilegge la stessa pagina. Ogni record porta una chiave di consegna derivata dal cursore di pagina e dalla sua posizione, e un registro conserva ogni scrittura completata per sette giorni: i record già scritti vengono quindi riprodotti dal registro invece di essere inviati una seconda volta alla destinazione.

Perché una sincronizzazione ripetuta non scrive due volte lo stesso contatto?

Prima di ogni chiamata alla destinazione, l’esecuzione confronta la chiave di consegna del record con un registro. La chiave è un hash della sincronizzazione, della sorgente, del cursore di pagina, della posizione nella pagina e del record stesso, quindi un crash, un nuovo tentativo un minuto dopo e un’esecuzione pianificata il giorno seguente derivano la stessa chiave per lo stesso record. Una chiave già segnata come completata entro la finestra di sette giorni non viene inviata di nuovo.

Che cos’è un allarme uomo morto per una sincronizzazione?

Un controllo che scatta per un’assenza anziché per un errore. Una volta all’ora Tajo esamina ogni workspace con una sincronizzazione attiva e lancia un allarme quando nessuna esecuzione è riuscita nelle ultime ventiquattro ore, compreso il caso in cui non è stata registrata alcuna esecuzione. Scatta una volta per incidente e si riarma dopo la prima esecuzione riuscita. Esiste perché un allarme sugli errori può vedere solo gli errori che sono stati registrati.

Richiedi l’accesso anticipato

Inserisci il tuo nome e il tuo indirizzo email. Ti invieremo un link per confermare la richiesta di accesso a Tajo.

Ottieni Brevo

Questa pagina in Markdown: worker-died-sync-didnt.md

Caricamento del Markdown…
Apri in Claude Apri in ChatGPT