Edit

Share via


JournaledGrain basics

Journaled grains derive from JournaledGrain<TGrainState,TEventBase>, with the following type parameters:

  • TGrainState represents the state of the grain. It must be a class with a public default constructor.
  • TEventBase is a common supertype for all events that can be raised for this grain and can be any class or interface.

All state and event objects should be serializable because log-consistency providers might need to persist them and/or send them in notification messages.

For grains whose events are POCOs (plain old C# objects), you can use JournaledGrain<TGrainState> as a shorthand for JournaledGrain<TGrainState,TEventBase>.

Reading the grain state

To read the current grain state and determine its version number, JournaledGrain has these properties:

GrainState State { get; }
int Version { get; }

The version number always equals the total number of confirmed events, and the state is the result of applying all confirmed events to the initial state. The default constructor of the GrainState class determines the initial state, which has version 0 (because no events have been applied to it).

Important: Your application should never directly modify the object returned by State. It's meant for reading only. When your application needs to modify the state, it must do so indirectly by raising events.

Raise events

Raise events by calling the RaiseEvent function. For example, a grain representing a chat can raise a PostedEvent to indicate that a user submitted a post:

RaiseEvent(new PostedEvent()
{
    Guid = guid,
    User = user,
    Text = text,
    Timestamp = DateTime.UtcNow
});

Note that RaiseEvent initiates a write to storage but doesn't wait for the write to complete. For many applications, it's important to wait for confirmation that the event has been persisted. In that case, always follow up by waiting for ConfirmEvents:

RaiseEvent(new DepositTransaction()
{
    DepositAmount = amount,
    Description = description
});
await ConfirmEvents();

Note that even if you don't explicitly call ConfirmEvents, the events eventually get confirmed automatically in the background.

State transition methods

The runtime updates the grain state automatically whenever events are raised. Your application doesn't need to explicitly update the state after raising an event. However, your application still needs to provide the code specifying how to update the state in response to an event. You can do this in two ways:

(a) The GrainState class can implement one or more Apply methods on the StateType. Typically, you create multiple overloads, and the runtime chooses the closest match for the runtime type of the event:

class GrainState
{
    Apply(E1 @event)
    {
        // code that updates the state
    }

    Apply(E2 @event)
    {
        // code that updates the state
    }
}

(b) The grain can override the TransitionState function:

protected override void TransitionState(
    State state, EventType @event)
{
   // code that updates the state
}

Assume transition methods have no side effects other than modifying the state object and should be deterministic (otherwise, the effects are unpredictable). If the transition code throws an exception, Orleans catches it and includes it in a warning in the Orleans log, issued by the log-consistency provider.

When exactly the runtime calls the transition methods depends on the chosen log-consistency provider and its configuration. Applications shouldn't rely on specific timing unless the log-consistency provider explicitly guarantees it.

Some providers, like the Orleans.EventSourcing.LogStorage log-consistency provider, replay the event sequence every time the grain loads. Therefore, as long as the event objects can still be properly deserialized from storage, you can radically modify the GrainState class and the transition methods. However, for other providers, such as the Orleans.EventSourcing.StateStorage log-consistency provider, only the GrainState object is persisted. In this case, you must ensure it can be deserialized correctly when read from storage.

Raise multiple events

You can make multiple calls to RaiseEvent before calling ConfirmEvents:

RaiseEvent(e1);
RaiseEvent(e2);
await ConfirmEvents();

However, this likely causes two successive storage accesses and incurs a risk that the grain fails after writing only the first event. Thus, it's usually better to raise multiple events at once using:

RaiseEvents(IEnumerable<EventType> events)

This guarantees the given sequence of events is written to storage atomically. Note that since the version number always matches the length of the event sequence, raising multiple events increases the version number by more than one at a time.

Retrieve the event sequence

The following method from the base JournaledGrain class allows your application to retrieve a specified segment of the sequence of all confirmed events:

Task<IReadOnlyList<EventType>> RetrieveConfirmedEvents(
    int fromVersion,
    int toVersion);

However, not all log-consistency providers support this method. If it's not supported, or if the specified segment of the sequence is no longer available, a NotSupportedException is thrown.

To retrieve all events up to the latest confirmed version, call:

await RetrieveConfirmedEvents(0, Version);

You can only retrieve confirmed events: an exception is thrown if toVersion is larger than the current value of the Version property.

Since confirmed events never change, there are no races to worry about, even with multiple instances or delayed confirmation. However, in such situations, the value of the Version property might be larger by the time the await resumes than when RetrieveConfirmedEvents was called. Therefore, it might be advisable to save its value in a variable. See also the section on Concurrency Guarantees.