I det forrige indlæg viste jeg Cirqus’ konfigurations-API, som helt objektivt set i høj grad kan klassificeres som “lækker”, måske endda “herre-nice”. Nu skal vi snakke lidt mere om event-processering – eller “projektioner”, som det også kaldes – altså det her, der sker med events efter at de er blevet genereret.
“Projektioner” er i dette eksempel begrænset til views, altså materialiserede read-modeller i en database, men man vil formentlig også lave alle former for integration til downstream eksterne systemer via projektioner, f.eks. afsendelse af emails, events ud på en servicebus, dokumentgenerering, osv.
Skalerbarhed
CQRS-baserede løsninger bliver ofte fremhævet for deres unikke skalerbarhed, hvilket også kan være tilfældet, men det skal lige nævnes at denne skalerbarhed altså først og fremmest gør sig gældede på read-siden af systemet.
Hvis man ønsker en konsistent domænemodel (og det gør man ofte, er min erfaring), så er flaskehalsen i system helt klart command-processeringen – alle commands må nødvendigvis behandles af en fuldt opdateret domænemodel, og emittede events skal serialiseres (pr. aggregate root som minimum, men også gerne serialisering på globalt niveau), hvilket har som konsekvens at der kan opstå contention omkring sekvensnummertildeling og efterfølgende gemning af events.
Men da antagelsen er at command-processeringen er mindre belastet end læse-delen af de fleste systemer, så gør det ikke så meget – skalerbarheden kan dermed opnås ved at lade eventuelle projektioner opdatere sig asynkront efter processeringen af hver command. Klienter, som ønsker at læse, vil på den måde altid kunne læse data – de vil blot i nogle tilfælde læse noget, som er et par events bagefter virkeligheden.
Et par events bagefter virkeligheden?
Jeps, men det er ofte ikke noget problem – de fleste mennesker har alligevel vænnet sig til at de tal, der står på skærmen, har en vis staleness – vi har efterhånden fået uddannet brugerne til at forstå at tallene stammer fra det tidspunkt, hvor skærmbilledet blev indlæst, og at man skal trykke F5 eller lignende for at få opdateret værdierne såfremt der er foretaget handlinger andre steder i systemet.
MEN der er alligevel et tidspunkt, hvor man som bruger ofte forventer at se “friske tal”, og det er når man selv har ændret noget. Hvis jeg f.eks. ændrer mit telefonnummer på min brugerprofil, og jeg efterfølgende ser det gamle nummer igen, så bliver jeg måske forvirret og forsøger at ændre det igen for at få systemet til at fatte det. Her vil det IKKE virke særlig intuitivt hvis view’et er et par events bagefter virkeligheden.
Med andre ord, så vil man som bruger opfatte systemet som up-to-date hvis det blot altid viser effekten af egne ændringer, mens at effekten af andres ændringer godt må være lidt bagefter (“lidt” – ligesom i aller aller højest et par sekunder eller noget, men i praksis ofte 100 ms eller sådan noget).
Hvordan gør man så det, når man arbejder med 100% asynkrone views?
Asynkrone views
Med Cirqus er alle views som udgangspunkt asynkrone. Konsekvensen af dette er at man, såfremt man skulle finde på at forespørge på et view umiddelbart efter processeringen af en command, formentlig ikke vil få fat i de ændringer, der måtte være sket som følge af den netop processerede command.
Hvordan løser man så det?
Det er sådan set ikke så slemt :)
med Cirqus resulterer hvert kald til ProcessCommand
i et CommandProcessingResult
, som indeholder det højeste globale sekvensnummer for events emitted som resultat af den command, og med dette CommandProcessingResult
i hånden kan man nu stille sig til at vente på at et eller flere specifikke views catcher up.
Rent praktisk, så kan det gøres ved at man laver et ViewManagerWaitHandle
, som man giver med til alle view managers – det kan eksempelvis se sådan her ud i et tilfælde, hvor jeg dedikerer 4 tråde til asynkron event-processering, hvoraf de tre “pools” af views vil registrere sig i vores waitHandle
, hvilket medfører at vi har mulighed for at stille os til at vente på de views, som de styrer:
var waitHandle = new ViewManagerWaitHandle(); var commandProcessor = CommandProcessor.With() .EventStore(e => e.UseSqlServer("mssql", "Events") .EventDispatcher(e => { e.ViewManagerEventDispatcher(waitHandle, enMasseViewsHer); e.ViewManagerEventDispatcher(waitHandle, nogleFlereViewsHer); e.ViewManagerEventDispatcher(waitHandle, endnuFlereViewsHer); e.ViewManagerEventDispatcher(evigtAsynkroneViewsHer); }) .Create();
Når tiden så kommer til at vi skal vente, så kan det gøres således:
var result = commandProcessor.ProcessCommand(enEllerAndenCommand); await waitHandle.WaitForAll(result, TimeSpan.FromSeconds(3));
for at “blokere” tråden indtil alle views som minimum har processeret de events, der kom ud af at processere enEllerAndenCommand
(dog uden at blokere tråden rigtigt – læs evt. mere om C# async/await).
Hvis jeg bare er interesseret i at vente på et bestemt view, så kan jeg gøre sådan her:
await waitHandle.WaitFor<EtSpecifiktView>(result, TimeSpan.FromSeconds(1));
hvilket ofte vil være hurtigere, dog afhængig af konfigurationen af views naturligvis.
Nu bliver det rigtig sjovt
Hvis man lige bruger 10 minutter på sit applikations-framework, så kan man skrue tingene ret lækkert sammen – sådan her kan man med Cirqus og et web-baseret system, der understøtter async/await
, lave super-duper responsive og skalerbare systemer, som føles 100% konsistent og frisk for den enkelte bruger:
Dvs. klienten skal efter processeringen af den første command og i resten af sin levetid blot huske det højeste globale sekvensnummer, som den selv har medvirket til at frembringe – i efterfølgende forespørgsler kan dette nummer så vedlægges, hvorefter man på server-siden kan stille sig til at vente på at dette nummer som minimum er blevet processeret af de relevante views. På denne vis parallelliserer man opdateringen af views med HTTP response og efterfølgende HTTP request, og i praksis vil der ret sjældent være egentlig ventetid i forbindelse med forespørgsler.
Hvis man bygger denne mekanisme ind i applikationens framework og f.eks. bruger headers på HTTP requests til føre nummeret frem og tilbage, og f.eks. hooker await
-kaldet ind i IoC containerens factory methods, som vi formoder står for injection af den enkelte IViewManager
, så kan det gøres fuldstændig transparent for alle normale kodescenarier – med andre ord kan denne simulerede konsistens-dims bygges en gang for alle, og efterfølgende skal udviklere ikke tænke på eventual consistency i deres daglige arbejde.
PS: Man kan vente på den enkelte IViewManager
sådan her (hvilket naturligvis er hvad ViewManagerWaitHandle
gør bag gardinerne): await viewManager.WaitUntilProcessed(result, timeout)
.
Konklusion
Dette indlæg er foreløbig det sidste i min CQRS-føljeton om praktisk CQRS i .NET med Cirqus. Jeg håber jeg har forklaret begreberne nogenlunde, og jeg håber at jeg har formået at kommunikere at anvendelsen af event sourcing og CQRS har nogle fantastiske fordele og at det ikke er så besværligt at bruge det i praksis, når blot man har et fornuftigt framework til at stille nogle fornuftige rammer til rådighed.
Hvis man er interesseret i mere snak om event sourcing, CQRS og Cirqus, så kan jeg anbefale at man dukker op til et af de resterende to .NET user group events om emnet: AANUG eller CNUG (du kan desværre ikke nå at komme til ANUG– eller ONUG-arrangementerne – beklager…)