Votre flux delta rate les commits tardifs : construisez un curseur fiable
Une ligne est arrivée en retard parce que le filigrane n'était pas une frontière de commit. Suivez la défaillance, puis définissez couverture, frontière, rétention et reprise sûre.
Voici une défaillance que j'ai vue se produire deux fois, dans deux moteurs différents. Une exportation s'exécute à 10:00 et enregistre l'horodatage de dernière modification le plus élevé qu'elle a vu. Une transaction commencée à 09:58 est encore ouverte, valide à 10:03 et porte un horodatage du moment de son début. L'exportation suivante ne demande que les lignes plus récentes que 10:00 et ne la voit jamais. La taille du transfert était excellente et les données étaient fausses.
Suivez l'exportation qui a perdu une ligne
Suivez l'ordre des événements plutôt que le SQL. L'application écrit l'heure de modification de la ligne au début de la transaction, pas au moment du commit. L'exportateur lit un filigrane d'horloge murale alors que la transaction est encore ouverte. Puis la transaction valide avec une heure de modification antérieure au filigrane enregistré. Le lot suivant du consommateur commence après le filigrane, donc la ligne reste derrière le curseur et ne sera plus jamais sélectionnée.
Rien ne journalise d'erreur, parce que rien n'a échoué. C'est ce qui rend cette classe de bugs coûteuse : le flux delta est rapide, les comptes de lignes semblent plausibles, et la ligne manquante refait surface des semaines plus tard sous forme de ticket de rapprochement que personne ne peut reproduire.
Les numéros de séquence ont la même forme de défaut pour une autre raison. Un ID monotone vous indique l'ordre d'attribution des valeurs, pas l'ordre de validation des transactions. Une transaction qui a pris un numéro bas peut valider après une qui a pris un numéro haut, donc un curseur fondé sur l'ordre d'attribution peut aussi laisser un trou.
Cinq promesses que le flux doit tenir
Avant de choisir un transport, demandez au système source un contrat en cinq clauses. S'il manque une clause, vous avez une heuristique, pas un flux de changements.
- Couverture : insertions, mises à jour et suppressions, chacune avec la clé stable nécessaire pour l'appliquer.
- Frontière : ce que signifie une valeur de curseur et ce que contient un lot cohérent.
- Cohérence : comment les tables liées sont lues sans mélanger des états source incompatibles.
- Rétention : combien de temps un consommateur déconnecté peut reprendre depuis sa position enregistrée.
- Réinitialisation : un instantané complet pris en charge pour quand cette position n'est plus valide.
Un mécanisme pris en charge peut satisfaire les cinq. La documentation de SQL Server sur le suivi des modifications est un exemple publié concret : elle définit une version minimale valide, enregistre les suppressions aussi bien que les modifications et décrit une transaction d'instantané qui couvre la validation, la capture de version et les requêtes de modifications associées. C'est un exemple de ce qu'il faut exiger d'une source, pas une syntaxe à coller dans un autre moteur.
Concevez le point de contrôle comme partie du pipeline
La position enregistrée du consommateur est la véritable machine à états, alors écrivez-la comme telle. Le curseur ne peut avancer que par le flux de travail qui a déjà appliqué le lot et validé la transaction cible.
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
Si la livraison échoue à mi-parcours, réconciliez ce même lot au lieu d'inventer une nouvelle position source. Réextraire avec une borne supérieure plus récente est la façon dont une application partielle devient un trou permanent. La règle miroir vaut aussi de l'autre côté : une application idempotente à la cible évite les doublons et ne fait rien pour un changement que l'extraction n'a jamais vu.
Les suppressions exigent des enregistrements, pas des déductions
Comparer des lignes qui existent encore ne peut pas dire à un consommateur quelles clés ont disparu. Une source qui n'enregistre jamais les suppressions vous laisse deux options imparfaites : conserver un historique de clés assez longtemps pour faire un diff, ou exécuter un rapprochement complet explicite selon un calendrier. Les deux sont des choix de conception avec un coût, et aucun n'existe si personne ne le possède.
La rétention est le même type de décision. Chaque flux de changements pris en charge a une fenêtre ; déclarez-la dans le contrat et alertez quand le point de contrôle d'un consommateur s'en approche. Un consommateur qui se réveille après une interruption de deux semaines devrait lire une erreur claire "réinitialisation nécessaire" au lieu de recevoir silencieusement un lot partiel.
Mesurez ensuite si le delta valait la peine
Revenir à comparer chaque clé et chaque hash est parfois juste, et il vaut la peine de savoir ce que cela coûte. Vous transférez toujours des informations sur chaque ligne, les hash ne vous donnent pas d'instantané cohérent entre tables, et un schéma de hash apporte ses propres questions de représentation et de collisions. Moins d'octets avec une frontière vérifiable est l'objectif ; moins d'octets qui laissent tomber silencieusement un changement validé n'est pas du tout un delta.
Une colonne de dernière modification est un indice. Un curseur qui survit à un commit tardif, à une suppression et à une longue interruption est un contrat.