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 Statezurü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.