Ga naar hoofdinhoud

Het snapshot-mechanisme

Waarom snapshots

Een betrouwbaar register dient de resultaten van queries herhaalbaar te maken. Een afnemer moet, nadat deze op tijdstip A een gegeven heeft opgevraagd, op tijdstip B nog steeds de vraag kunnen stellen wat dit gegeven op tijdstip A was. Een betrouwbaar register dient dit te kunnen nadat:

  1. Er nieuwe gevolgen zijn toegevoegd aan de registratie.
  2. Een gegeven hersteld is doordat het geldigheidstijdvak is veranderd vanwege een foutieve registratie.
  3. Een softwarebug in de opvraging is hersteld waardoor de initiële opvraging fout was.

Criterium 1 en 2 introduceren bitemporaliteit in de registratie. Criterium 3 vereist dat er mechanismen zijn die het mogelijk maken softwareaanpassingen te doen met behoud van de eerdere gegevensrepresentatie.

Inherente beperkingen

Elke registratie loopt tegen inherente beperkingen aan. Zowel geldigheids- als gevolgtijdstippen hebben te maken met eventual consistency: er zit altijd tijd tussen het begin van een registratie en het einde daarvan, en er zit altijd tijd tussen een opvraging en de zichtbaarheid van deze gegevens voor de afnemer. Dit betekent dat een afnemer altijd in potentie naar verouderde informatie kijkt.

Daarnaast heeft een registratie meestal niet de volledige controle over de aflevering van informatie. Entiteiten verlaten op een bepaald moment de registratie om elders te worden gepresenteerd — in een browser, een API-response, of een ander systeem dat de registratie niet kent.

Herhaalbaarheid van opvraging is dus hoogstens te behalen tot op het niveau van de gegevensrepresentatie net voordat deze het systeem verlaat.

Wat is een snapshot

Een snapshot legt de toestand van een (in een projectie gekozen) verzameling gegevens vast op een specifiek geldigheidstijdstip, als gevolg van een specifiek gevolg. Een snapshot wordt aangemaakt als Snapshot, maar wordt opgeslagen en teruggelezen als SnapshotMetSequence. De extra velden projectietijdstip en sequenceNumber zijn pas bekend ná opslag en worden door de snapshotstore toegevoegd.

De kernvelden van een snapshot:

VeldBetekenis
aggregateIdWelke entiteit dit snapshot beschrijft
gevolgIdWelk gevolg dit snapshot heeft veroorzaakt
geldigheidstijdstipOp welk moment in de werkelijkheid deze toestand geldig is
gevolgtijdstipWanneer dit gevolg in de registratie is vastgelegd
payloadDe inhoud van het snapshot (geserialiseerd), van type payloadType
metadataAanvullende informatie over de herkomst (zie Snapshot metadata)
vorigSnapshotIndexVerwijzing naar de voorgaande snapshot (voor herleidbaarheid)

De drie tijdsassen

Een snapshot kent drie tijdsassen:

  • Geldigheidstijdstip — wanneer de toestand geldig was in de werkelijkheid
  • Gevolgtijdstip — wanneer het gevolg in de registratie is vastgelegd
  • Projectietijdstip — wanneer het snapshot zichtbaar is geworden in de projectie

Door deze drie assen te combineren in een query is tijdreizen mogelijk: "hoe zag dit gegeven eruit op tijdstip X, zoals vastgelegd op tijdstip Y, zichtbaar in de projectie op tijdstip Z?"

De snapshot-operaties

Het toevoegen van een snapshot aan de snapshotstore beantwoordt twee onafhankelijke vragen: wat is het uitgangspunt voor de nieuwe snapshot, en op welke positie op de tijdlijn wordt hij geplaatst?

Dimensie 1 — Waar op de tijdlijn?

TermBetekenis
insertEerste punt van een nieuwe entiteit — altijd linksonder in de grafiek
appendNieuw tijdvak aan het einde van de geldigheidstijdlijn — altijd rechtsboven
amendBovenschrijft een bestaand punt met een nieuwe registratielaag
upendBovenschrijft een reeks geldigheidstijdstippen met een nieuwe registratielaag

Dimensie 2 — Wat is het uitgangspunt?

TermBetekenis
newGeen voorgaande snapshot — het gevolg leidt tot een geheel nieuwe projectietoestand
updateDe voorgaande snapshot op hetzelfde geldigheidstijdstip dient als uitgangspunt
recreateDe voorganger in geldigheid dient als uitgangspunt — de foutieve snapshot wordt genegeerd en de toestand opnieuw opgebouwd vanuit het punt vóór de fout

De vijf operaties

Niet alle combinaties van de twee dimensies zijn zinvol. De vijf geldige operaties zijn:

insertappendamendupend
newnewToInsertnewToUpend
updateupdateToAppendupdateToAmend
recreaterecreateToUpend

De niet-bestaande combinaties zijn logisch onmogelijk:

  • new + append en new + amend: new heeft geen voorganger om op voort te bouwen.
  • update + insert: insert is per definitie het eerste punt van een aggregate — er is niets om op voort te bouwen.
  • recreate + insert, recreate + append en recreate + amend: recreate bouwt voort op de voorganger in geldigheid, wat alleen zinvol is bij bovenschrijving van een reeks tijdstippen (upend).

newToInsert

new to insert

Een entiteit verschijnt voor het eerst in de registratie. Er is geen voorganger; vorigSnapshotIndex is null. Dit is altijd het beginpunt van de grafiek — linksonder op de tijdlijn.

newToUpend

new to upend

Een nieuwe entiteit wordt ingevoegd op een specifiek tijdvak, als onderdeel van een bovenschrijving. Gebruik deze operatie bij herstel wanneer de herstelde toestand een nieuwe entiteit introduceert op een bestaand tijdstip — het object bestond in de registratie nog niet, maar had er wel moeten zijn.

updateToAppend

update to append

Een bestaande entiteit wordt bijgewerkt. Het nieuwe snapshot bouwt voort op het meest recente snapshot op hetzelfde geldigheidstijdstip. Geeft null terug als er geen inhoudelijke wijziging is ten opzichte van de voorganger.

updateToAmend

update to amend

Een bestaand punt op de tijdlijn wordt bovenschreven. De bestaande snapshot op hetzelfde geldigheidstijdstip dient als uitgangspunt voor de nieuwe snapshot. Gebruik deze operatie wanneer extra informatie beschikbaar komt over hetzelfde geldigheidstijdstip.

recreateToUpend

recreate to upend

Herstel: snapshots worden opnieuw opgebouwd op basis van het voorgaande snapshot in geldigheid. De foutieve snapshot wordt genegeerd; de toestand wordt opnieuw opgebouwd vanuit het punt vóór de fout. Zie Herstel voor de context van wanneer deze operatie wordt ingezet.

De snapshotstore

Snapshots worden opgeslagen in een append-only snapshotstore: eenmaal opgeslagen worden snapshots nooit aangepast of verwijderd, alleen toegevoegd. Dit sluit aan bij het principe van Event Sourcing, waarbij de volledige geschiedenis van een registratie behouden blijft en volledig herleidbaar is.

De SnapshotRepository-interface biedt methoden voor het opslaan en opvragen van snapshots over de drie tijdsassen. Door geldigheidstijdstip, gevolgtijdstip en projectietijdstip te combineren in een query is het mogelijk precies te vragen naar hoe een gegeven eruitzag op een bepaald moment in de werkelijkheid, zoals dat op een bepaald moment was vastgelegd, en zoals dat zichtbaar was in de projectie op een bepaald moment.

Dit maakt het mogelijk om de herhaalbaarheid van queries te garanderen tot op het niveau beschreven in de inherente beperkingen hierboven.

Snapshot metadata

Elke snapshot bevat metadata die vastlegt welk gevolg hem heeft veroorzaakt. Dit maakt het mogelijk om achteraf precies te reconstrueren hoe de registratie tot stand is gekomen.

Het basisgeval

In het basisgeval — bij alle reguliere gevolgen — bevat de metadata alleen het gevolg-type:

SnapshotMetadata(WozObjectGeregistreerd::class)

Dit legt vast dat dit snapshot is aangemaakt als reactie op een WozObjectGeregistreerd-gevolg.

Bij herstel via subgevolgen

Bij herstel via subgevolgen bevat één herstelgevolg meerdere subgevolgen die elk hun eigen snapshots produceren. Om elke snapshot uniek herleidbaar te maken naar zijn herkomst binnen dat herstelgevolg, worden twee extra velden gevuld:

SnapshotMetadata(
BelangNaarGecorrigeerd::class, // het omsluitende herstelgevolg
BelangOvergedragen::class, // het subgevolg zoals het had moeten zijn
2, // de positie van dit subgevolg in de reeks
)

De drie velden:

VeldBetekenis
gevolgTypeHet type van het (herstel)gevolg dat de verwerking heeft geïnitieerd
subGevolgTypeHet type van het subgevolg zoals het eigenlijk had moeten zijn
subGevolgIndexDe positie van het subgevolg in de reeks (0-gebaseerd)

Dit maakt het mogelijk om bij herstel achteraf precies te reconstrueren hoe het geweest had moeten zijn.