Je deltafeed mist late commits: bouw een betrouwbare cursor
Eén rij kwam te laat omdat het watermerk geen commit-grens was. Volg de fout en definieer daarna dekking, grens, bewaring en een veilige hervatting.
Hier is een fout die ik twee keer heb zien gebeuren, in twee verschillende engines. Een export draait om 10:00 en legt de hoogste laatst-gewijzigd-tijdstempel vast die hij zag. Eén transactie die om 09:58 begon, is nog open, commit om 10:03 en draagt een tijdstempel uit het moment dat hij begon. De volgende export vraagt alleen om rijen nieuwer dan 10:00 en ziet hem nooit. De overdrachtsgrootte was uitstekend en de gegevens waren fout.
Volg de export die een rij kwijtraakte
Volg de volgorde van gebeurtenissen, niet de SQL. De applicatie schrijft de wijzigingstijd van de rij bij de start van de transactie, niet bij de commit. De exporteur leest een muurklok-watermerk terwijl die transactie nog open is. Dan commit de transactie met een wijzigingstijd die ouder is dan het opgeslagen watermerk. De volgende batch van de consument begint na het watermerk, dus de rij blijft achter de cursor en wordt nooit meer geselecteerd.
Niets logt een fout, want niets is mislukt. Dat maakt deze klasse bugs duur: de deltafeed is snel, de rijaantallen lijken plausibel, en de ontbrekende rij duikt weken later op als een afstemmingsticket dat niemand kan reproduceren.
Volgnummers hebben dezelfde vorm van gebrek om een andere reden. Een monotone ID vertelt je de volgorde waarin waarden zijn toegewezen, niet de volgorde waarin transacties committen. Een transactie die een laag nummer pakte, kan committen na een die een hoog nummer pakte, dus een cursor op basis van toewijzingsvolgorde kan ook een gat achterlaten.
Vijf beloften die de feed moet nakomen
Vraag het bronsysteem, voordat je een transport kiest, om een contract met vijf clausules. Ontbreekt er een clausule, dan heb je een heuristiek en geen wijzigingsfeed.
- Dekking: inserts, updates en deletes, elk met de stabiele sleutel die nodig is om ze toe te passen.
- Grens: wat een cursorwaarde betekent en wat een consistente batch bevat.
- Consistentie: hoe gerelateerde tabellen worden gelezen zonder onverenigbare brontoestanden te mengen.
- Bewaring: hoe lang een losgekoppelde consument kan hervatten vanaf zijn opgeslagen positie.
- Herinitialisatie: een ondersteunde volledige momentopname voor wanneer die positie niet meer geldig is.
Een ondersteund mechanisme kan alle vijf vervullen. De documentatie van SQL Server over change tracking is één concreet gepubliceerd voorbeeld: het definieert een minimale geldige versie, registreert deletes zowel als wijzigingen en beschrijft een momentopname-transactie die validatie, versievastlegging en de bijbehorende wijzigingsquery's omspant. Het is een voorbeeld van wat je van een bron moet eisen, geen syntaxis die je in een andere engine plakt.
Ontwerp het checkpoint als onderdeel van de pijplijn
De opgeslagen positie van de consument is de echte toestandsmachine, dus schrijf hem als zodanig. De cursor mag alleen vooruit via de workflow die de batch al heeft toegepast en de doeltransactie heeft gecommit.
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
Mislukt de levering halverwege, verzoen dan diezelfde batch in plaats van een nieuwe bronpositie te bedenken. Opnieuw extraheren met een frissere bovengrens is de manier waarop een gedeeltelijke toepassing een permanent gat wordt. De spiegelregel geldt ook aan de andere kant: idempotente toepassing aan de doelzijde voorkomt duplicaten en doet niets voor een wijziging die de extractie nooit zag.
Deletes vragen om registraties, niet om conclusies
Rijen vergelijken die nog bestaan, kan een consument niet vertellen welke sleutels verdwenen. Een bron die nooit deletes registreert, laat je twee onvolmaakte opties: een sleutelgeschiedenis lang genoeg bewaren om tegen te diffen, of een expliciete volledige afstemming volgens schema draaien. Beide zijn ontwerpkeuzes met een prijs, en geen ervan bestaat tenzij iemand hem bezit.
Bewaring is dezelfde soort beslissing. Elke ondersteunde wijzigingsfeed heeft een venster; noem het in het contract en waarschuw wanneer het checkpoint van een consument het nadert. Een consument die na een onderbreking van twee weken wakker wordt, hoort een duidelijke fout "herinitialisatie nodig" te lezen in plaats van stil een gedeeltelijke batch te ontvangen.
Meet daarna of de delta het waard was
Terugvallen op het vergelijken van elke sleutel en hash is soms juist, en het is de moeite waard te weten wat het kost. Je draagt nog steeds informatie over elke rij over, hashes geven je geen consistente momentopname over tabellen heen, en een hashschema brengt zijn eigen vragen over weergave en botsingen mee. Minder bytes met een verifieerbare grens is het doel; minder bytes die stil een gecommitte wijziging laten vallen is helemaal geen delta.
Een kolom voor laatste wijziging is een hint. Een cursor die een late commit, een delete en een lange onderbreking overleeft, is een contract.