5. EventSourcing : définition
● Une idée maîtresse : prendre comme source de vérité la succession des
événements qui surviennent dans un système
● Plutôt que s’appuyer sur un représentation muable du présent, on
s’appuie sur l’enregistrement des événements passés et immuables.
5
6. RDBMS vs EventSourcing
Source http://docs.geteventstore.com/introduction/img/transactional-model-with-delete.png
VS
Source https://martin.kleppmann.com/2015/03/insideout-06.png
6
8. EventSourcing, au niveau des API 1/2
trait Stream[M[_]] {
val name: String
def append(event: Event, events: Event*): M[WriteCompleted]
def read(position: Int): M[Event]
}
case class Event(evenType: String, id: UUID, body: Content, metadata: Content)
case class WriteCompleted(start: Int, end: Int)
8
9. EventSourcing, au niveau des API 2/2
trait Stream[M[_]] {
def readPage(fromPosition: Int, pageSize: Int,
readDirection: ReadDirection = Forward): M[Page]
def subscribeTo(observer: SubscriptionObserver): Closeable
def subscribeToFrom(observer: SubscriptionObserver, position: Int): Closeable
}
case class Page(events: List[Event], fromPosition: Int,
nextPosition: Int, readDirection: ReadDirection)
trait SubscriptionObserver {
def onLiveProcessingStart(subscription: Closeable)
def onEvent(event: Event, subscription: Closeable)
def onError(e: Throwable)
def onClose()
}
9
11. Invariant(s)
∀p ∈ ℕ, et pour une définition de
‘=‘
x := read(p)
y := read(p)
‘x ≠ ⊥’ ⇒ x = y
Plus simplement :
La cacheabilité est infinie.
Une fois qu’un élément est
disponible à une position (p), cet
élément sera identique (=) pour
toujours.
11
12. EventSourcing : les avantages
Conserver l’intégralité de l’historique d’un système offre de nombreux
avantages :
● Plus proche de la réalité (perception),
● Auditabilité,
● Observabilité de la nouveauté,
● Simplicité de consommation,
● Réinterprêtation de l’historique (BI for “Free”).
● Facilite (beaucoup) CQRS
Decision
Apply
commande
état
event
Read
Model
Read
Model
Read
Model 12
13. EventSourcing : les inconvénients
L’eventsourcing présente aussi quelques inconvénients :
● Consommation d’espace disque plus importante (et en constante
augmentation),
● Démarrage potentiellement plus long (il faut relire tout l’historique),
● Difficile de changer le schéma des événements (<= immuabilité).
● Découpage en streams, et opérations sur plusieurs streams.
● Multiplication des états (read models, Monoid[Event]).
● Gestion asynchrone.
13
15. Patterns
● Algébre d’événement (List[Event] à la place de A * List[Event]),
● Génération d’id non coordonnée => UUID,
● Quantification de la donnée,
● Capture des réglès métier complexes, et décisions peu reproductibles.
● Corrélation et causation
● Sagas
15
18. Pourquoi ajouter des métadonnées ?
● Les events contiennent la donnée métier, mais on a aussi besoin de
données techniques pour :
○ Expliciter le contexte d’écriture (instance, saga, commande, position dans la commande).
○ Périniser la donnée (schéma à l’écriture).
○ Tracer la provenance (de qui, de la part de).
○ Séparer la donnée entre différent environnement partageant la même infrastructure.
18
19. Quels types de métadonnées ?
● Métadonnées des événements
○ Corrélation & causation
○ Informations de type (schéma, on y reviendra)
○ etc...
● Méta-streams (ou streams de méta-events) d’informations techniques sur
le système
○ Instances
○ Schémas
19
21. Pourquoi/comment typer les streams ?
● Sans typage, pas d’albèbre, pas moyen d’écrire
○ state = events.foldLeft(initial)(_ apply _)
● Problème : comment conserver le type des “vieux” events ?
● Quand le schéma des events change, on doit pouvoir
○ Upcaster les anciens events dans le nouveaux schéma (pour pouvoir démarrer une
nouvelle instance sur les anciennes données)
○ Downcaster les nouveaux events (pour que les anciennes instances continuent de
fonctionner pendant la mise à jour)
21
22. API typée : définition
case class Event[E](event:E, id:UUID, metadata:Metadata)
trait TypedStream[M[_], E] {
implicit val coproductEvidence: Generic[E]
implicit val serializableEvidence:AkiraSerializable[E]
val name: String @@ StreamId
def append(event: Event[E], events: Event[E]*): M[WriteCompleted]
def read(position: Int): M[Event[E]]
}
22
23. API typée : usage
sealed trait MQTTEvent
case class Disconnected(clientID: String) extends MQTTEvent
case class Published(clientID: String,
payload: Seq[Byte],
topicName: String) extends MQTTEvent
val mqttStream:TypedStream[Id,MQTTEvent] = ___
mqttStream.append(___)
23
24. Comment sérialiser les événements ?
L’EventSourcing nécessite une bonne sérialisation, car les événements vont
être écrits et consommés par :
● Beaucoup de programmes (dans des langages différents).
● Beaucoup d’intermédiaires.
● Pendant plusieurs années.
Une bonne sérialisation est :
● Extensible.
● Auto descriptible.
● Peut être lue et écrite de manière générique et indépendante.
24
25. Comment sérialiser les messages ?
Pour l’instant, nous utilisons Avro 1.8.x avec l’encodeur JSON,
mais c’est un détail d’implémentation.
25
26. ● Conserver les schémas (passés et actuels) de tous les types d’événements
○ Dans les métadonnées de chaque événement, placer une référence vers le schéma dudit
événement
○ Dans un métastream dédié, stocker une représentation générique de chaque schéma
(sous la forme d’événements SchemaCreated
● Il faut aussi conserver le schéma des métadonnées
● Au démarrage, on utilise le méta stream des schémas pour initialiser le
Schéma Registry
● Problème de l’oeuf et de la poule
○ quel schéma pour les événements du stream schémas ?
○ sur quel stream les enregistre-t-on ?
Schema registry
26
27. Séquence de démarrage
schemas
meta
stream-xyz
SchemaCreated
<Metadata>
hash : “ab12ce9”
Schema : { “typ..
SchemaCreated
<InstanceUp>
hash : “56f3e05”
Schema : { “typ..
InstanceUp
data : nodeXYZ
meta : 56f3e05
SchemaCreated
<SmthHappened>
hash : “76b4ed0”
Schema : { “typ..
SmthHappened
data : {‘d’:”123”}
meta : 76b4ed0
SmthHappened
data : {‘d’:”abc”}
meta : 76b4ed0
SchemaCreated
<SchemaCreated>
hash : “2d163b9”
Schema : { “typ..
Application démarrée
27
28. Séquence de démarrage
● Au démarrage, on initialise le Schema Registry
● Comme le méta stream schémas est vide on enregistre les schémas “par
defaut” :
○ Le schéma de SchemaCreated
○ Le schéma des métadonnées
● On veut ensuite enregistrer le démarrage d’une nouvelle instance
(InstanceUp)
○ Le schéma d’InstanceUp n’existe pas, il faut l’enregistrer
○ On émet un InstanceUp dans le méta schéma
● L’application est démarrée
● Avant d’enregistrer un nouvel event, on vérifie que son schéma est
présent dans le registry, et on l’enregistre si besoin
28
29. Evolution des types
A + BA A + B + C
auto auto
BA
f: A → B
Record with aliased fieldsRecord
auto
wideningnarrowing
A × BA × B × C A
auto auto
projectionadd field
29
30. Evolution des types
A × BA × B × C A
auto auto
projectionadd field
A × B × Czero
auto
A × B × (C + Czero
)
auto
auto
auto
add field with
default value
auto
Czero
<: C
A @@ B auto
A
auto
A as logical type A @@ B
Type level
Serialization level
A @@ B <: A
30
31. Evolution des types dans le temps
A1
A2
A3
autoauto auto
× A4
?
f
B1
stream-xyz : A1
+ B1
A2
+ B1
A3
+ B1
autoauto A4
? + B1
A’4
+ A3
+ B1
Gérer la compatibilité
entre A3
et A4
.
Poser A4
à coté de A3
.
Deux solutions :
auto
31
32. Conclusion
● L’EventSourcing c’est bon, mangez-en !
● Les principaux problèmes pénibles ont des solutions
● Coming soon : Akira (https://github.com/vil1/akira)
32