dotnet / orleans

Cloud Native application framework for .NET
https://docs.microsoft.com/dotnet/orleans
MIT License
9.92k stars 2.01k forks source link

[Question] Orleans Event Sourcing snapshots of event log #4862

Open keithazzopardi opened 5 years ago

keithazzopardi commented 5 years ago

I have a JournaledGrain which I am persisting events through the attribute [LogConsistencyProvider(ProviderName = "LogStorage")]. Since this grain will handle heavy load, hydrating the grain every time with all the events can result in a performance bottleneck. As a result, snapshot are one alternative to this problem by hydrating events since the latest snapshot only. Is there is a way to handle snapshots in Orleans Event Sourcing?

I am using MongoDB to persist the data.

sergeybykov commented 5 years ago

@sebastianburckhardt What are your thoughts on snapshots?

keithazzopardi commented 5 years ago

Having a snapshots of the state at any point in time so that the current state is built by getting the last X events since the last snapshots. At the moment, Orleans.EventSourcing.LogStorage.LogConsistencyProvider builds the state by reading all events which might create some performance problems in production.

As an alternative to snapshots, storing all the grain's generated events as well as the current state of the grain. In this way, the principles of event sourcing are adhered to by having an audit trail of the state anytime and be able to reply events when necessary. I see this as a hybrid of Orleans.EventSourcing.StateStorage.LogConsistencyProvider and Orleans.EventSourcing.LogStorage.LogConsistencyProvider.

sebastianburckhardt commented 5 years ago

If you use (ProviderName = "StateStorage") then the system always stores snapshots. In fact, it stores snapshots only (no events).

Currently we don't have a hybrid solution (store both snapshots and events), but we worked out a pretty clear plan on how to support this a while back. The problem is that the progress on event sourcing seems sort of stalled... #3773 3 (minor revisions to API) has been waiting for review. and #2711 (new provider API for event storage, including test kit, and provider implementation for EventStore) needs substantial updates due to big changes to the provider configuration story. We would need some sort of joint plan & commitment to start moving this forward again.

keithazzopardi commented 5 years ago

@sebastianburckhardt Yes, I am using (ProviderName = "StateStorage") as the application with have high volumes.

I am using EventSourcing API and my grains inheresit from JournaledGrain<TGrainState, TEventBase>. Is there a way to persist the occurred events manually into another database?

So far I have implemented this by overriding the RaiseEvent method and inserting the event into another database. However, I'm afraid that should the process of updating the state fails, there is no way to rollback of that inserted event. Any ideas please?

sebastianburckhardt commented 5 years ago

Right, in order to guarantee consistency, the event storage needs to be managed by the runtime. We don't currently have the combination of extensible snapshot + event storage supported, though we do plan to support it in the future.

If you need to get something working sooner, you can try to plug in your own storage backend to provide that functionality. Essentially, you have to write two functions, one for storing events, and one for reading the aggregate state of all events (which you can construct from a snapshot and/or the event log).

To declare that you want to write your own load and store methods you specify [LogConsistencyProvider(ProviderName = "CustomStorage")]

Then, implement the interface ICustomStorageInterface on the grain.

(To see a complete sample of how you may use this, look at the sample in 1.x/ReplicatedEventSample and specifically the class EventGrain.cs)

   /// <summary>
    /// The storage interface exposed by grains that want to use the CustomStorage log-consistency provider
    /// <typeparam name="TState">The type for the state of the grain.</typeparam>
    /// <typeparam name="TDelta">The type for delta objects that represent updates to the state.</typeparam>
    /// </summary>
    public interface ICustomStorageInterface<TState, TDelta>
    {
        /// <summary>
        /// Reads the current state and version from storage
        /// (note that the state object may be mutated by the provider, so it must not be shared).
        /// </summary>
        /// <returns>the version number and a  state object.</returns>
        Task<KeyValuePair<int,TState>> ReadStateFromStorage();

        /// <summary>
        /// Applies the given array of deltas to storage, and returns true, if the version in storage matches the expected version. 
        /// Otherwise, does nothing and returns false. If successful, the version of storage must be increased by the number of deltas.
        /// </summary>
        /// <returns>true if the deltas were applied, false otherwise</returns>
        Task<bool> ApplyUpdatesToStorage(IReadOnlyList<TDelta> updates, int expectedversion);
    }

This will guarantee consistency as long as the storage operations check the version numbers (note that version number = length of event log)

isen-ng commented 5 years ago

I was reading this issue, and am trying to implement my own hybrid snapshot event storage using ICustomStorageInterface.

However, I just ran into a road block when trying to write a snapshot in Task<bool> ApplyUpdatesToStorage(IReadOnlyList<TDelta> updates, int expectedversion); because the original and/or tenative states are not available. (Original state is better as I can build the state i want to snapshot using the updates)

    public async Task<bool> ApplyUpdatesToStorage(IReadOnlyList<TDelta> updates, int expectedVersion)
        {
            using (var session = _documentStore.OpenSession())
            {
                session.Events.Append(_grainTypeName, expectedVersion, updates);
                await session.SaveChangesAsync();

                _version = expectedVersion;

                if (_snapshotStrategy.ShouldTakeSnapshot(_version))
                {
                   // this line is not possible because the tentative state is not available
                    session.Store(new Snapshot(_grainTypeName, _version, tentativeState)); 
                    await session.SaveChangesAsync();
                }

                return true;
            }
        }

The work around I can think of is to build the final state again based on the last snapshot up till the current event that was just committed again, which is a waste of resources.

Am I doing this correctly? Or am I doing this wrongly? This is my gist. I'm trying to integrate with Marten.

(This is my first time using Orleans and my foray back into C# after a very long time)

isen-ng commented 5 years ago

Oh wait, i just realized that I have to implement ICustomStorage on the Grain itself.

I can't register a global CustomStorageProvider with the HybridStorage I've written and re-use it with different grains like [LogConsistencyProvider(ProviderName = "HybridStorage")]?

This means that now my gain needs to know/implement it's own storage, which isn't very modular.

isen-ng commented 5 years ago

In the end, I implemented by own LogConsistencyProvider and LogViewAdaptor that uses a Marten document store for storage.

I copied and integrated code from LogStorage, StateStorage and CustomStorage for this to work.

johnberzy-bazinga commented 4 years ago

@isen-ng We're looking to do the same for a different storage system. Is the source available somewhere? Would be great to use it as a reference implementation.

Thanks!

isen-ng commented 4 years ago

Sorry @johnberzy-bazinga I can't share the implementation in a public forum due to a confidentiality contract.

However, I can tell you that I literally copied all the code from CustomStorage and just expanded it with some other code in LogStorage and StateStorage. (eg use log storage's code to cumulatively log all the events, and use state storage's code to store a snapshot). Then the only work left is to construct the read model from these 2 effectively.

johnberzy-bazinga commented 4 years ago

@isen-ng Totally understand. Thanks for the pointers!

MV10 commented 4 years ago

I'm not sure what the typical workflow is for this repo, but can this be tagged as a feature request (or whatever your equivalent tag would be, enhancement, perhaps)? Snapshots are a pretty important part of real-world event source usage. I was surprised that this isn't available out of the box given you already have nearly all of the parts between LogStorage and StateStorage.

zh6335901 commented 4 years ago

I have implemented the snapshot storage provider for Orleans.EventSourcing, This provider use grain storage to store the snapshot state, And can use independent event storage to store the events. You can see the detail form here

sergeybykov commented 4 years ago

@zh6335901 Would you like to add Orleans.EventSourcing.Snapshot to https://github.com/orleanscontrib for better visibility?

zh6335901 commented 4 years ago

@zh6335901 Would you like to add Orleans.EventSourcing.Snapshot to https://github.com/orleanscontrib for better visibility?

@sergeybykov Of cause! But how to add project to https://github.com/orleanscontrib?

sergeybykov commented 4 years ago

Great! I believe @richorama or @galvesribeiro can create a repo there for you and give you admin permissions to it.

richorama commented 4 years ago

Happy to help, I’m out on vacation right now, so can sort in a week or so..

Sent from my iPhone

On 1 Jan 2020, at 05:41, Sergey Bykov notifications@github.com wrote:

 Great! I believe @richorama or @galvesribeiro can create a repo there for you and give you admin permissions to it.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or unsubscribe.

galvesribeiro commented 4 years ago

Hey folks!

@zh6335901 I've created https://github.com/OrleansContrib/Orleans.EventSourcing.Snapshot and imported your code there. Also added you as admin to it.

Please let me know if you have any issues.

Thanks!

zh6335901 commented 4 years ago

@galvesribeiro OK, Thanks!

ghost commented 1 year ago

We've moved this issue to the Backlog. This means that it is not going to be worked on for the coming release. We review items in the backlog at the end of each milestone/release and depending on the team's priority we may reconsider this issue for the following milestone.