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:
- Er nieuwe gevolgen zijn toegevoegd aan de registratie.
- Een gegeven hersteld is doordat het geldigheidstijdvak is veranderd vanwege een foutieve registratie.
- 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:
| Veld | Betekenis |
|---|---|
aggregateId | Welke entiteit dit snapshot beschrijft |
gevolgId | Welk gevolg dit snapshot heeft veroorzaakt |
geldigheidstijdstip | Op welk moment in de werkelijkheid deze toestand geldig is |
gevolgtijdstip | Wanneer dit gevolg in de registratie is vastgelegd |
payload | De inhoud van het snapshot (geserialiseerd), van type payloadType |
metadata | Aanvullende informatie over de herkomst (zie Snapshot metadata) |
vorigSnapshotIndex | Verwijzing 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?
| Term | Betekenis |
|---|---|
| insert | Eerste punt van een nieuwe entiteit — altijd linksonder in de grafiek |
| append | Nieuw tijdvak aan het einde van de geldigheidstijdlijn — altijd rechtsboven |
| amend | Bovenschrijft een bestaand punt met een nieuwe registratielaag |
| upend | Bovenschrijft een reeks geldigheidstijdstippen met een nieuwe registratielaag |
Dimensie 2 — Wat is het uitgangspunt?
| Term | Betekenis |
|---|---|
| new | Geen voorgaande snapshot — het gevolg leidt tot een geheel nieuwe projectietoestand |
| update | De voorgaande snapshot op hetzelfde geldigheidstijdstip dient als uitgangspunt |
| recreate | De 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:
| insert | append | amend | upend | |
|---|---|---|---|---|
| new | newToInsert | — | — | newToUpend |
| update | — | updateToAppend | updateToAmend | — |
| recreate | — | — | — | recreateToUpend |
De niet-bestaande combinaties zijn logisch onmogelijk:
- new + append en new + amend:
newheeft geen voorganger om op voort te bouwen. - update + insert:
insertis per definitie het eerste punt van een aggregate — er is niets om op voort te bouwen. - recreate + insert, recreate + append en recreate + amend:
recreatebouwt voort op de voorganger in geldigheid, wat alleen zinvol is bij bovenschrijving van een reeks tijdstippen (upend).
newToInsert
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
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
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
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
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:
| Veld | Betekenis |
|---|---|
gevolgType | Het type van het (herstel)gevolg dat de verwerking heeft geïnitieerd |
subGevolgType | Het type van het subgevolg zoals het eigenlijk had moeten zijn |
subGevolgIndex | De 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.