Grundlegendes zu JournaledGrain
Journalisierte Grains leiten sich von JournaledGrain<TGrainState,TEventBase> ab und verfügen über folgende Typparameter:
TGrainState
stellt den Zustand des Grains dar. Hierbei muss es sich um eine Klasse mit einem öffentlichen Standardkonstruktor handeln.TEventBase
ist ein allgemeiner Obertyp für alle Ereignisse, die für dieses Grain ausgelöst werden können, und kann eine beliebige Klasse oder Schnittstelle sein.
Alle Zustands- und Ereignisobjekte müssen serialisierbar sein, da die Protokollkonsistenzanbieter sie möglicherweise persistent speichern und/oder in Benachrichtigungsmeldungen senden müssen.
Bei Grains, bei deren Ereignisse es sich um einfache C#-Objekte (Plain Old C# Objects, POCOs) handelt, kann JournaledGrain<TGrainState> als Abkürzung für JournaledGrain<TGrainState,TEventBase> verwendet werden.
Lesen des Grain-Zustands
JournaledGrain verfügt über Eigenschaften zum Lesen des aktuellen Grain-Zustands sowie zum Bestimmen der Versionsnummer.
GrainState State { get; }
int Version { get; }
Die Versionsnummer entspricht immer der Gesamtanzahl bestätigter Ereignisse, und der Zustand ist das Ergebnis der Anwendung aller bestätigten Ereignisse auf den Anfangszustand. Der Anfangszustand hat die Version 0 (da noch keine Ereignisse darauf angewendet wurden) und wird durch den Standardkonstruktor der GrainState-Klasse bestimmt.
Wichtig: Die Anwendung darf das von State
zurückgegebene Objekt niemals direkt ändern. Es darf nur gelesen werden. Zustandsänderungen durch die Anwendung müssen indirekt durch Auslösen von Ereignissen erfolgen.
Auslösen von Ereignissen
Das Auslösen von Ereignissen wird durch Aufrufen der Funktion RaiseEvent erreicht. Ein Grain, das einen Chat darstellt, kann beispielsweise ein Ereignis vom Typ PostedEvent
auslösen, um anzugeben, dass ein Benutzer einen Beitrag übermittelt hat:
RaiseEvent(new PostedEvent()
{
Guid = guid,
User = user,
Text = text,
Timestamp = DateTime.UtcNow
});
Beachten Sie, dass RaiseEvent
einen Schreibzugriff auf den Speicher startet, aber nicht wartet, bis der Schreibvorgang abgeschlossen ist. Bei vielen Anwendungen ist es wichtig zu warten, bis die Bestätigung vorliegt, dass das Ereignis persistent gespeichert wurde. In diesem Fall wird im Anschluss auf ConfirmEvents gewartet:
RaiseEvent(new DepositTransaction()
{
DepositAmount = amount,
Description = description
});
await ConfirmEvents();
Beachten Sie, dass die Ereignisse letztlich auch bestätigt werden, wenn Sie nicht explizit ConfirmEvents
aufrufen. Das passiert automatisch im Hintergrund.
Zustandsübergangsmethoden
Die Runtime aktualisiert den Grain-Zustand automatisch, wenn Ereignisse ausgelöst werden. Die Anwendung muss den Status nicht explizit aktualisieren, nachdem ein Ereignis ausgelöst wurde. Die Anwendung muss allerdings weiterhin Code bereitstellen, der angibt, wie der Zustand infolge eines Ereignisses aktualisiert werden soll. Dazu gibt es zwei Möglichkeiten.
(a) Die Klasse GrainState kann eine oder mehrere Apply
-Methoden für StateType
implementieren. In der Regel werden mehrere Überladungen erstellt, und es wird die beste Übereinstimmung für den Laufzeittyp des Ereignisses ausgewählt:
class GrainState
{
Apply(E1 @event)
{
// code that updates the state
}
Apply(E2 @event)
{
// code that updates the state
}
}
(b) Das Grain kann die Funktion TransitionState
außer Kraft setzen:
protected override void TransitionState(
State state, EventType @event)
{
// code that updates the state
}
Es wird davon ausgegangen, dass die Übergangsmethoden lediglich das Zustandsobjekt ändern und deterministisch sind. Andernfalls sind die Auswirkungen unvorhersehbar. Wenn der Übergangscode eine Ausnahme auslöst, wird sie abgefangen und in eine Warnung im vom Protokollkonsistenzanbieter ausgegebenen Orleans-Protokoll eingeschlossen.
Wann genau die Runtime die Übergangsmethoden aufruft, hängt vom ausgewählten Protokollkonsistenzanbieter sowie von dessen Konfiguration ab. Anwendungen dürfen sich nicht auf einen bestimmten Zeitpunkt verlassen, es sei denn, dieser wird vom Protokollkonsistenzanbieter ausdrücklich garantiert.
Von manchen Anbietern wird die Ereignissequenz jedes Mal wiedergegeben, wenn das Grain geladen wird. Ein Beispiel ist etwa der Protokollkonsistenzanbieter Orleans.EventSourcing.LogStorage. Daher ist es möglich, die Klasse GrainState
und die Übergangsmethoden grundlegend zu ändern, solange die Ereignisobjekte weiterhin ordnungsgemäß aus dem Speicher deserialisiert werden können. Bei anderen Anbietern wie etwa dem Protokollkonsistenzanbieter Orleans.EventSourcing.StateStorage wird dagegen nur das Objekt GrainState
persistent gespeichert. Entwickler*innen müssen daher sicherstellen, dass es beim Lesen aus dem Speicher ordnungsgemäß deserialisiert werden kann.
Auslösen mehrerer Ereignisse
RaiseEvent
kann mehrmals aufgerufen werden, bevor ConfirmEvents
aufgerufen wird:
RaiseEvent(e1);
RaiseEvent(e2);
await ConfirmEvents();
Dies führt jedoch wahrscheinlich zu zwei aufeinanderfolgenden Speicherzugriffen, und es besteht die Gefahr, dass für das Grain ein Fehler auftritt, nachdem nur das erste Ereignis geschrieben wurde. Daher ist es in der Regel besser, wie folgt mehrere Ereignisse gleichzeitig auszulösen:
RaiseEvents(IEnumerable<EventType> events)
Dadurch wird garantiert, dass die angegebene Sequenz von Ereignissen atomisch in den Speicher geschrieben wird. Hinweis: Da die Versionsnummer immer der Länge der Ereignissequenz entspricht, erhöht sich die Versionsnummer beim Auslösen mehrerer Ereignisse gleichzeitig um mehrere Stufen.
Abrufen der Ereignissequenz
Mit der folgenden Methode aus der Basisklasse JournaledGrain
kann die Anwendung ein angegebenes Segment der Sequenz aller bestätigten Ereignisse abrufen:
Task<IReadOnlyList<EventType>> RetrieveConfirmedEvents(
int fromVersion,
int toVersion);
Sie wird allerdings nicht von allen Protokollkonsistenzanbietern unterstützt. Wenn sie nicht unterstützt wird oder das angegebene Segment der Sequenz nicht mehr verfügbar ist, wird eine Ausnahme vom Typ NotSupportedException ausgelöst.
Wenn Sie alle Ereignisse bis zur neuesten bestätigten Version abrufen möchten, können Sie Folgendes aufrufen:
await RetrieveConfirmedEvents(0, Version);
Es können nur bestätigte Ereignisse abgerufen werden. Ist toVersion
größer als der aktuelle Wert der Eigenschaft Version
, wird eine Ausnahme ausgelöst.
Da sich bestätigte Ereignisse nie ändern, müssen Sie sich keine Gedanken über Race-Bedingungen machen, auch wenn mehrere Instanzen vorhanden sind oder die Bestätigung verzögert erfolgt. In solchen Situationen kann der Wert der Eigenschaft Version
zum Zeitpunkt der Fortsetzung von await
allerdings größer sein als zum Zeitpunkt des Aufrufs von RetrieveConfirmedEvents
. Daher empfiehlt es sich gegebenenfalls, den zugehörigen Wert in einer Variablen zu speichern. Weitere Informationen finden Sie auch im Abschnitt zu Parallelitätsgarantien.