Bitemporele opslag van snapshots
Dit is een uitwerking van een mogelijke variant van implementatie van bitemporaliteit in projecties.
Eerder hebben we elementen van bitemporele projecties uitgelegd; de temporaliteit in projecties, en de standaardoperaties die gebruikt kunnen worden bij het bijwerken van projecties. In dit hoofdstuk beschrijven we een implementatie van deze patronen: de bitemporele opslag van snapshots, kortweg snapshotting. Deze implementatie kun je terugvinden in Gitlab.
Snapshotting is één invulling van een projectiestore: de plek waar een projectie wordt opgeslagen. Een projectiestore kan ook relationeel of hybride zijn (zie de varianten); snapshotting is dus geen synoniem van projectiestore, maar een specifieke opslagvorm daarvan.
Wat is een snapshot
Een snapshot is een instantie van een projectie-element: hij legt de toestand van een (in een projectie gekozen) verzameling gegevens vast op een specifiek projectietijdstip, als resultaat van een specifiek gevolg. Denk hierbij aan een foto waarop een bepaalde situatie (en een bepaalde omvang) is vastgelegd.
Een snapshot bestaat uit twee delen:
- de payload: de daadwerkelijke inhoud, de gegevens zoals de projectie die verstrekt;
- de metadata: boekhouding over de payload, geen inhoudelijke gegevens. Metadata is een verzamelnaam voor twee dingen: de herkomst en de tijdstippen, naar de paragraaf herkomst en tijd in het conceptueel registermodel.
Append-only opslag
Snapshots worden append-only opgeslagen: eenmaal opgeslagen worden snapshots nooit aangepast of verwijderd, alleen toegevoegd. Elke snapshot is via predecessorId gekoppeld aan het snapshotId van zijn voorganger. Dit is geïnspireerd op event sourcing, waarbij de volledige geschiedenis van een registratie behouden blijft en volledig herleidbaar is. Dit principe geldt dus niet alleen voor het gevolgenjournaal, maar ook voor bitemporele projecties.
De SnapshotRepository-interface biedt methoden voor het opslaan en opvragen van snapshots over de temporele assen. Voor het opslaan van nieuwe snapshots zijn de eerder beschreven standaardoperaties geïmplementeerd.
Volgorde
De volgorde waarin snapshots worden opgeslagen doet ertoe. De tijdstippen alleen zijn niet uniek: bij een correctie of replay ontstaan vaak meerdere snapshots uit één gevolg, met hetzelfde gevolgtijdstip, en snapshots die in één keer worden opgeslagen krijgen hetzelfde projectietijdstip: Het projectietijdstip is een transactietijdstip. Snapshot id's zijn sequence numbers: ze leggen een volgorde vast. Bestaan er voor hetzelfde geldigheidstijdstip meerdere snapshots, dan is die met het hoogste volgnummer de laatst vastgelegde en daarmee de actuele op de as van de systeemtijd.
Het volgnummer is geen derde temporele as. Het is de fijnste volgorde binnen de systeemtijd: het projectietijdstip zegt wanneer een snapshot zichtbaar werd, het volgnummer in welke volgorde.
Dit alles maakt het eerder beschreven tijdreizen mogelijk en daarmee de herhaalbaarheid van bevragingen tot op het niveau beschreven in de inherente beperkingen van de herhaalbare vraag.
Waarom bitemporele opslag van snapshots
De handreiking beschrijft de herhaalbare vraag als kerneis van een betrouwbaar register: een afnemer moet op een later tijdstip nog steeds kunnen opvragen wat een gegeven op een eerder tijdstip was. Die eis, inclusief de criteria en de inherente beperkingen, vereist een opslagmechanisme dat twee temporele assen expliciet bijhoudt.
Snapshotting is zo'n mechanisme. Elke snapshot combineert een geldigheidstijdstip (domeintijd) met een gevolgtijdstip en een projectietijdstip (beide systeemtijd). Daarmee kan deze vorm van opslag voldoen aan de drie criteria uit de handreiking: nieuwe gevolgen, gecorrigeerde geldigheid en herstelde softwarefouten resulteren elk in nieuwe snapshots die naast de bestaande worden geplaatst, zonder de bestaande te muteren.
Snapshots zijn daarmee geen cache of tijdelijk tussenproduct: ze zijn juist het leesmodel van de projectie, en de primaire route waarlangs een projectie wordt bevraagd.
Het schrijven van snapshots
Het snapshot-projectiemechanisme implementeert het contract zoals beschreven in het voorgaande hoofdstuk. Elke nieuwe snapshot wordt append only toegevoegd aan de store:
Het lezen van snapshots
Snapshotting is in eerste instantie gebouwd om voor een entiteit per tijdsdimensie precies één snaphot te kunnen lezen. Eén van de krachten van deze projectievorm is dat dit soort queries erg eenvoudig zijn. Het is ook eenvoudig mogelijk om snapshots te bevragen op bijvoorbeeld de gevolg-keten die het snapshot geproduceerd heeft. Filteren op andere metadata-elementen is ook goed mogelijk. Daarnaast kunnen tijdlijnen opgevraagd worden.
Het is moeilijker aan de opvraagkant om projectie-elementen te combineren. Bitemporele tabellen zijn daar geschikter voor.
De velden van een snapshot
Een snapshot bestaat uit payload en metadata, ofwel de volgende velden:
| Veld | Deel | Betekenis |
|---|---|---|
snapshotId | – | Volgnummer en identiteit van de snapshot, toegekend bij opslag |
entityId | – | Welke entiteit deze snapshot beschrijft |
payloadType | – | Tot welke projectie deze snapshot behoort; samen met entityId bepaalt dit de reeks |
payload | payload | De inhoud van de snapshot (geserialiseerd) |
provenance | herkomst | Waarom en hoe deze snapshot is ontstaan |
validFromTimestamp | tijdstippen | Vanaf wanneer de toestand geldig is in de werkelijkheid |
effectTimestamp | tijdstippen | Wanneer het gevolg in het gevolgenjournaal is vastgelegd |
projectionTimestamp | tijdstippen | Wanneer de snapshot zichtbaar is geworden in de projectie |
predecessorId | – | Het snapshotId van de voorgaande snapshot; leeg bij de eerste snapshot van een reeks |
effectLineage | – | De reeks onderschreven gevolgen waarop deze snapshot voortbouwt |
Het veld provenance bevat de herkomst; de tijdstippen staan als losse velden op de snapshot. Geen veld heet metadata: metadata is de verzamelnaam voor beide. De velden zonder deel (snapshotId, entityId, payloadType, predecessorId, effectLineage) zijn geen payload en geen metadata: ze leggen de identiteit van de snapshot vast en koppelen hem aan zijn projectie, zijn entiteit, zijn voorganger en de gevolgen waarop hij voortbouwt. payloadType en entityId wijzen samen de reeks aan: alle snapshots binnen één projectie over dezelfde entiteit. Binnen die reeks koppelt predecessorId elke snapshot aan zijn voorganger.
Het snapshotId en het projectionTimestamp krijgt een snapshot pas wanneer hij wordt opgeslagen. Daarvóór is hij nog niet vastgelegd, en wordt hij als kandidaat aan de opslag aangeboden. De opslag kan hem aannemen of weigeren, bijvoorbeeld wanneer hij gelijk is aan de actieve snapshot op hetzelfde geldigheidstijdstip. In de code heet een niet-vastgelegde snapshot UnrecordedSnapshot (bij het aanbieden in een variabele candidate) en een vastgelegde snapshot Snapshot.
Herkomst
De herkomst van een snapshot is hetzelfde begrip als herkomst in het conceptueel registermodel, met dezelfde vier kenmerken: bron (waarop de snapshot gegrond is), verantwoordelijke (wie ervoor instaat), legitimering (op grond van welk kader) en werkwijze (volgens welke procedure of welk algoritme, in welke versie). Een projectie is een eigen verwerking met een eigen herkomst. De verwijzing naar het toegepaste gevolg maakt de herkomst van dat gevolg bereikbaar, maar vervangt die van de projectie niet: de projectie past eigen regels toe, op een eigen wettelijke grondslag, met eigen software.
Voor het indelen van velden hanteren we één regel: identiteit (een id of index – welke specifieke instantie) hoort bij de bron; classificatie (een type – welk soort verwerking is toegepast) hoort bij de werkwijze.
In de code heten de velden zoals in de begrippenlijst (Engels); in de voorbeelddata en de viewers staan ze onder hun Nederlandse naam.
| Veld (code) | Veld (voorbeelddata) | Kenmerk | Betekenis |
|---|---|---|---|
predecessorId | voorgangerId | bron | Op welke voorgaande snapshot deze snapshot gegrond is |
lastAppliedEffect | afgespeeldeGevolgId | bron | Welk specifiek gevolg is toegepast |
effectPartIndex, effectSubPartIndex | gevolgdeelIndex, gevolgsubdeelIndex | bron | Welk specifiek gevolgdeel (en subdeel) binnen dat gevolg |
effectTriggeringReplay | gevolgIdDatReplayTriggerde | bron | Welk gevolg een replay in gang heeft gezet |
isCorrection, isWithdrawalOfWithdrawal | isCorrectie, isOnttrekkingVanOnttrekking | bron | Kwalificeren het gevolg waarop de snapshot gegrond is |
lastAppliedEffectType | gevolgType | werkwijze | Welk soort gevolg is toegepast |
effectPartType, effectSubPartType | gevolgdeelType, gevolgsubdeelType | werkwijze | Welk soort gevolgdeel (en subdeel) |
software | softwareComponent, softwareComponentVersie | werkwijze | Welke software, in welke versie, de snapshot heeft geproduceerd |
legitimation | wetNaamTenGrondslagAanProjectie, wetUrlTenGrondslagAanProjectie | legitimering | Welke wet de projectie toepast |
| – | – | verantwoordelijke | Nog niet vastgelegd |
De voorbeeldimplementatie legt de wettelijke grondslag en de softwarecomponent en -versie vast voor gevolgen én voor snapshots, met dezelfde bouwstenen. Elke projectie declareert haar eigen grondslag, en de softwarecomponent van een snapshot draagt de naam van de projectie. De grondslag van het toegepaste gevolg wordt niet overgenomen: die blijft via het toegepaste gevolg bereikbaar.
De voorganger staat twee keer op een snapshot, met dezelfde waarde maar in een andere rol. Op de snapshot zelf is predecessorId een koppeling: hij rijgt de snapshots van een reeks aan elkaar. In de herkomst is hij een bron: een nieuwe snapshot is gegrond op het toegepaste gevolg én op de inhoud van zijn voorganger. Zo is de herkomst te lezen zonder de rest van de snapshot erbij te nemen.
Software en versie horen bij de werkwijze, niet bij de verantwoordelijke: ze beschrijven hoe een snapshot tot stand kwam, niet wie ervoor instaat. Automatisering verplaatst de verantwoordelijkheid niet naar het systeem. De voorbeeldimplementatie legt de verantwoordelijke – de organisatie of rol die voor de productie instaat – op dit moment niet vast, voor gevolgen noch voor snapshots.
Bij een regulier gevolg blijven de velden voor gevolgdeel, subdeel en replay leeg. Bij correcties (voor de gevolgrepresentatie van correcties, zie hier) bevat één correctiegevolg meerdere gevolgdelen die elk hun eigen snapshots produceren. De velden voor gevolgdeel en -subdeel maken elke snapshot dan uniek herleidbaar naar het gevolgdeel waarop hij gegrond is. Zo kan achteraf precies worden gereconstrueerd hoe het had moeten zijn.
Tijdstippen
De drie tijdstippen op een snapshot hebben hun Nederlandse naam uit het conceptueel registermodel en de verdieping tijdstippen en geldigheid:
| Veld | Tijdstip | Soort tijd |
|---|---|---|
validFromTimestamp | geldigheidstijdstip | domeintijd |
effectTimestamp | gevolgtijdstip | systeemtijd, journaalkant |
projectionTimestamp | projectietijdstip | systeemtijd, projectiekant |
Tijd is complex. Vooral als tijden zich tot elkaar verhouden, zoals bitemporeel. Hier zijn vele doorsneden van te maken en valt veel over te zeggen. Laten we tenminste stellen dat deze drie tijdstippen er toe doen.
De domeintijd die zich manifesteert als geldigheidstijdstip, is een gekozen tijdstip gekoppeld aan een in het domein betekenisvol begrip. Dit zou bijvoorbeeld geboortedatum kunnen zijn. Of juist de geboorte-akte tijdstip. Het is een keuze van het domein voor de betreffende projectie. Dit kan dus per projectie verschillend zijn.
Systeemtijd manifesteert zich op twee plekken in de CQRS-architectuur, analoog aan een commit-timestamp versus een replication-timestamp in een gedistribueerde database. Aan de journaalkant is dat het gevolgtijdstip: het moment waarop het gevolg in het gevolgenjournaal is vastgelegd. Aan de projectiekant is dat het projectietijdstip: het moment waarop de snapshot zichtbaar is geworden in de projectie. Tussen deze twee momenten zit verwerkingstijd: bij synchrone verwerking verwaarloosbaar, bij asynchrone of gedistribueerde architectuur kan dat significant zijn.
Door deze drie tijdstippen te combineren in een bevraging is tijdreizen mogelijk: hoe zag dit gegeven eruit op geldigheidstijdstip X, zoals vastgelegd bij gevolg Y, zichtbaar in de projectie op tijdstip Z?