Ecco un guasto che ho visto accadere due volte, in due motori diversi. Un'esportazione viene eseguita alle 10:00 e registra la marca temporale di ultima modifica più alta che ha visto. Una transazione iniziata alle 09:58 è ancora aperta, esegue il commit alle 10:03 e porta una marca temporale del momento in cui è iniziata. L'esportazione successiva chiede solo righe più recenti delle 10:00 e non la vede mai. La dimensione del trasferimento era eccellente e i dati erano sbagliati.

Ripercorri l'esportazione che ha perso una riga

Segui l'ordine degli eventi, non l'SQL. L'applicazione scrive l'ora di modifica della riga quando inizia la transazione, non quando esegue il commit. L'esportatore legge una marca d'acqua a orologio da parete mentre quella transazione è ancora aperta. Poi la transazione esegue il commit con un'ora di modifica più vecchia della marca d'acqua salvata. Il lotto successivo del consumatore inizia dopo la marca d'acqua, quindi la riga resta dietro il cursore e non verrà mai più selezionata.

Niente registra un errore, perché niente è fallito. È questo che rende costosa questa classe di bug: il feed delta è veloce, i conteggi delle righe sembrano plausibili e la riga mancante riemerge settimane dopo come un ticket di riconciliazione che nessuno può riprodurre.

I numeri di sequenza hanno la stessa forma di difetto per un'altra ragione. Un ID monotono ti dice l'ordine in cui sono stati assegnati i valori, non l'ordine in cui le transazioni hanno eseguito il commit. Una transazione che ha preso un numero basso può eseguire il commit dopo una che ha preso un numero alto, quindi un cursore basato sull'ordine di assegnazione può anche lasciare un buco.

Cinque promesse che il feed deve mantenere

Prima di scegliere un trasporto, chiedi al sistema di origine un contratto con cinque clausole. Se ne manca una, hai un'euristica e non un feed di modifiche.

  • Copertura: inserimenti, aggiornamenti ed eliminazioni, ciascuno con la chiave stabile necessaria per applicarlo.
  • Confine: che cosa significa un valore di cursore e che cosa contiene un lotto coerente.
  • Coerenza: come si leggono le tabelle correlate senza mescolare stati di origine incompatibili.
  • Conservazione: per quanto tempo un consumatore disconnesso può riprendere dalla posizione salvata.
  • Reinizializzazione: un'istantanea completa supportata per quando quella posizione non è più valida.

Un meccanismo supportato può soddisfare tutte e cinque. La documentazione di SQL Server sul rilevamento delle modifiche è un esempio pubblicato concreto: definisce una versione minima valida, registra le eliminazioni oltre alle modifiche e descrive una transazione di istantanea che copre convalida, acquisizione della versione e le relative query di modifica. È un esempio di ciò che si deve pretendere da un'origine, non una sintassi da incollare in un altro motore.

Progetta il punto di controllo come parte della pipeline

La posizione salvata del consumatore è la vera macchina a stati, quindi scrivila come tale. Il cursore può avanzare solo attraverso il flusso di lavoro che ha già applicato il lotto e confermato la transazione di destinazione.

cursor  = durable_checkpoint()          # last fully applied position
batch   = extract(cursor, upper_bound)  # one consistent batch
apply(batch)                            # idempotent at the target
commit()
checkpoint(batch.upper_bound)           # only now

Se la consegna fallisce a metà, riconcilia quello stesso lotto invece di inventare una nuova posizione di origine. Rieseguire l'estrazione con un limite superiore più fresco è il modo in cui un'applicazione parziale diventa un buco permanente. La regola dello specchio vale anche dall'altro lato: l'applicazione idempotente sulla destinazione evita i duplicati e non fa nulla per una modifica che l'estrazione non ha mai visto.

Le eliminazioni richiedono registrazioni, non conclusioni

Confrontare righe che esistono ancora non può dire a un consumatore quali chiavi sono sparite. Un'origine che non registra mai le eliminazioni ti lascia due opzioni imperfette: mantenere una cronologia delle chiavi abbastanza a lungo per un diff, oppure eseguire una riconciliazione completa esplicita secondo un calendario. Entrambe sono scelte di progetto con un costo, e nessuna esiste se qualcuno non la possiede.

La conservazione è lo stesso tipo di decisione. Ogni feed di modifiche supportato ha una finestra; dichiarala nel contratto e avvisa quando il punto di controllo di un consumatore vi si avvicina. Un consumatore che si sveglia dopo un'interruzione di due settimane dovrebbe leggere un errore chiaro "reinizializzazione necessaria" invece di ricevere in silenzio un lotto parziale.

Poi misura se il delta valeva la pena

Tornare a confrontare ogni chiave e hash a volte è giusto, e vale la pena sapere quanto costa. Stai comunque trasferendo informazioni su ogni riga, gli hash non ti danno un'istantanea coerente tra le tabelle e uno schema di hash porta con sé domande su rappresentazione e collisioni. Meno byte con un confine verificabile è l'obiettivo; meno byte che lasciano cadere in silenzio una modifica confermata non è affatto un delta.

Una colonna di ultima modifica è un indizio. Un cursore che sopravvive a un commit tardivo, a un'eliminazione e a una lunga interruzione è un contratto.

Articoli correlati