Implementing an Event Store with optimistic concurrency checks over multiple clients

777 views Asked by At

I'm trying to implement an event sourcing system using the practices and principles in line with the various Greg Young inspired examples I have seen.

I understand how the version checking logic works and that when saving the aggregate, if the current version does not match the expected version it means another session/client app has updated the aggregate before you have.

I also understand that you can have in place a method of retrospectively resolving conflicts when concurrent events have been saved, this question is not so much about doing this.

What I am trying to understand is that in a specific implementation with using a nosql database such as ravendb as the event store, how would I ensure that the events written never overlap version numbers due to the race condition.

The following code from an example project to illustrate:

In the Repository<TAggregate> repository class there is a save method

    public void Save(AggregateRoot aggregate, int expectedVersion)
    {
        if (aggregate.GetUncommittedChanges().Any())
        {
            lock (_lockStorage)
            {
                var item = new T();

                if (expectedVersion != -1)
                {
//issue if two processes get to this line of code below together 
//and both have the same 'version to be expected' then start writing together
                    item = GetById(aggregate.Id); 
                    if (item.Version != expectedVersion)
                    {
                        throw new ConcurrencyException(string.Format("Aggregate {0} has been previously modified",
                                                                     item.Id));
                    }
                }

                _storage.Save(aggregate);
            }
        }
    }

Now essentially this works fine when there is only a single application. The lock stops any other thread writing events to the event store while the current thread has taken the lock, checked the version, and then written its own events.

However imagine two separate clients running on different machines. Obviously the two processes can both get into the lock together and they can both then execute the GetById() method and both see the same currently committed version. Both processes would then go on to write the uncommitted event(s) with incrementing version numbers. However this leaves the Event Stream for the aggregate in a state where there could be different events with the same version number.

I know I could do some kind of retrospective resolution but that isn't what i'm trying to accomplish.

Now clearly this means that some kind of locking needs to be done on the Event Store database side of things. Could anyone advise how to do this? Answer does not have to be database specific but a ravendb example would be great as that's what I'm planning to prototype my event sourcing system with.

2

There are 2 answers

0
Codescribler On

Is there any reason the two separate clients running on different machines can't interact with the application via an API? That way the central processing of the events are handled on 1 machine and avoid the issues you describe. If you are referring to a partially connected scenario then you still wouldn't do this logic on the client. If it is unavoidable you would need to combine the event streams at some point and then handle conflicts.

If it's helpful I do have a post on handling concurrency issues but it doesn't handle your exact scenario but may give you some ideas for concurrency conflict resolution.

1
Martijn van den Broek On

Don't implement optimistic concurrency control yourself when the underlying database already provides support for this.

Typically this is implemented by passing the version you expect the data to be at to the database server. Only when the version matches the current version the update will go through successfully.

RavenDB uses e-tags for optimistic concurrency control: