laravel-event-sourcing: Duplicate uuid/version events prevent further updates

First I want to thank you guys for your awesome packages and the hard work you put into them. ❤️

I was trying out this package in local development (Nginx + PHP-FPM, MySQL). After some time this system goes dormant and the next request takes some time to be processed. In this state I (accidentally) made 2 concurrent requests to update the same aggregate. The corresponding controller function:

    public function update(UserUpdateRequest $request, User $user)
    {
        UserAggregateRoot::retrieve($user->id)
            ->updateUser($request->validated() + ['id' => $user->id])
            ->persist();

        return UserResource::make($user->refresh());
    }

This resulted in a duplicate entry in the database, see:

image

Note: The UserAggregateRoot does NOT have protected static bool $allowConcurrency = true;

Now every update request to that specific aggregate (user) results in the following error:

[2020-11-19 17:39:58] local.ERROR: Could not persist aggregate UserAggregateRoot (uuid: 8be7ace5-8c98-3823-b858-11cdff2974e7) because it seems to be changed by another process after it was retrieved in the current process. Expect to persist events after version 2, but version 1 was already persisted. {"userId":"9e33aa4f-6efa-3505-ab49-9d667e5b2c39","exception":"[object] (Spatie\\EventSourcing\\Exceptions\\CouldNotPersistAggregate(code: 0): Could not persist aggregate UserAggregateRoot (uuid: 8be7ace5-8c98-3823-b858-11cdff2974e7) because it seems to be changed by another process after it was retrieved in the current process. Expect to persist events after version 2, but version 1 was already persisted. at base/vendor/spatie/laravel-event-sourcing/src/Exceptions/CouldNotPersistAggregate.php:18)
[stacktrace]
#0 base/vendor/spatie/laravel-event-sourcing/src/AggregateRoots/AggregateRoot.php(189): Spatie\\EventSourcing\\Exceptions\\CouldNotPersistAggregate::unexpectedVersionAlreadyPersisted()
#1 base/vendor/spatie/laravel-event-sourcing/src/AggregateRoots/AggregateRoot.php(90): Spatie\\EventSourcing\\AggregateRoots\\AggregateRoot->ensureNoOtherEventsHaveBeenPersisted()
#2 base/vendor/spatie/laravel-event-sourcing/src/AggregateRoots/AggregateRoot.php(79): Spatie\\EventSourcing\\AggregateRoots\\AggregateRoot->persistWithoutApplyingToEventHandlers()
#3 base/Modules/User/Http/Controllers/UserController.php(91): Spatie\\EventSourcing\\AggregateRoots\\AggregateRoot->persist()
  1. Should this be handled by the package automatically (is this a bug) or should this be prevented in another part of the system?
  2. Should I enforce unique constraints in the database on [aggregate_uuid, aggregate_version]? If so, should this be highlighted in the docs?
  3. FYI: adding allowConcurrency afterwards to the root allows for updating and inserts a new event with version 3.

Best regards, Marcel

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 27 (11 by maintainers)

Most upvoted comments

@freekmurze Did some research over the weekend. This package is unable to deal with the race condition. For instance if you have a race condition like two events with the same ID and the same version will be saved at nearly the same time. This can be simulated using apache benchmark, for example ab -n 20 -c 10

One way to solve this is to use a composite primary key consisting of the aggregate ID and the version number, however, this package does not save unique version number per aggregate.

So my suggestion is to do:

  • modify the package to save unique version number when persisting the aggregate at :

image

  • then we can make use of the database composite primary key to solve the concurrency issue.

What do you think? If okay, I will send a PR .

I have found another instance where the aggregate has 2 events under the same aggregate_version, but they were created 20 mins apart. So these are certainly not the same process and an atomic lock will not help.

What could cause the same aggregate version to be used across this length of time?

Same aggregate_uuid, same aggregate_version, created at 21:05 & 21:26 respectively

image