Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Could not persist aggregate because it seems to be changed by another process after it was retrieved in the current process. Current in-memory version is 1 #325

Closed
JustSteveKing opened this issue Mar 2, 2022 · 7 comments
Assignees

Comments

@JustSteveKing
Copy link

My first time using v7 of the package, and had some issues with aggregates last night.

Could not persist aggregate PostAggregate (uuid: 19dad81e-b6ab-4012-a04a-401e1d35747d) because it seems to be changed by another process after it was retrieved in the current process. Current in-memory version is 1

Using Laravel 9.2.0
PHP 8.1

Aggregate:

class PostAggregate extends AggregateRoot
{
    protected static bool $allowConcurrency = false;

    public function createPost(DataObjectContract $object): self
    {
        $this->recordThat(
            domainEvent: new PostWasCreated(
                object: $object,
            ),
        );

        return $this;
    }

    public function getStoredEventRepository(): StoredEventRepository
    {
        return app(PostStoredEventsRepository::class);
    }
}

Event:

class PostWasCreated extends ShouldBeStored
{
    public function __construct(
        public readonly DataObjectContract $object,
    ) {}
}

Service Provider:

class EventSourcingServiceProvider extends ServiceProvider
{
    /**
     * @return void
     */
    public function register(): void
    {
        Projectionist::addProjector(
            projector: PostHandler::class,
        );

        Projectionist::addReactor(
            EmailModerators::class,
        );
    }
}

Projector:

class PostHandler extends Projector
{
    public function onPostWasCreated(PostWasCreated $event): void
    {
        /**
         * @var CreatePostContract
         */
        $action = resolve(CreatePostContract::class);

        $action->handle($event->object);
    }
}

The event is persisted in the database during testing with no errors, but when I interact with my code from the web UI - I get this issue. The aggregate is being triggered by a Livewire component that uses a service class to trigger the aggregate:

Livewire Component:

class CreateForm extends Component implements HasForms
{
    use InteractsWithForms;

    public bool $moderation = false;
    public null|string $title = null;
    public null|string $description = null;
    public null|string $content = null;
    public null|int $category = null;

    public function mount(): void
    {
        $this->form->fill();
    }

    public function submit(
        PostFactoryContract $factory,
        PostAggregateServiceContract $service,
    ) {
        $service->createPost(
            $factory->make(
                attributes: array_merge(
                    $this->form->getState(),
                    ['user_id' => auth()->id()],
                )
            )
        );

        return redirect()->route('dashboard');
    }

    protected function getFormSchema(): array
    {
        return [
            Grid::make(4)->schema([
                TextInput::make('title')->label('Post Title')->columnSpan(2)->required(),
                Select::make('category')->label('Post Category')->options(Category::get()->pluck('title', 'id'))->columnSpan(2)->required(),
                TextInput::make('description')->label('Post Description')->columnSpan(4)->required()->maxLength(120),
                MarkdownEditor::make('content')->label('Post Content')->columnSpan(4)->required(),
                    Toggle::make('moderation')->label('Submit for moderation')->columnSpan(4),
                ]),
        ];
    }

    public function render(): View
    {
        return view('livewire.posts.create-form');
    }
}

Service class:

class PostAggregateService implements PostAggregateServiceContract
{
    public function createPost(DataObjectContract $object): void
    {
        PostAggregate::retrieve(
            uuid: Str::uuid()->toString(),
        )->createPost(
            object: $object,
        )->persist();
    }
}

As you can see the service class tries to persist the aggregate, and this is where it fails. In my test code all I am doing is making sure that the event is stored:

it('triggers the aggregate root to store the event that a post was created', function () {
    $category = Category::factory()->create();
    auth()->loginUsingId(User::factory()->create()->id);

    expect(PostStoredEvent::query()->count())->toEqual(0);

    Livewire::test(CreateForm::class)
        ->set([
              'title' => 'pest php',
              'category' => $category->id,
              'description' => 'pest PHP is awesome, prove me wrong',
              'content' => 'Here be content, pirates and dragons',
          ])->call('submit')->assertHasNoErrors();

    expect(
        PostStoredEvent::query()->count()
    )->toEqual(1);
})->group('publishing');
@JustSteveKing
Copy link
Author

Repo, if required: https://github.com/JustSteveKing/phponline.dev-new

@brendt
Copy link
Collaborator

brendt commented Mar 2, 2022

Here's a whole discussion on concurrency: #214

There are two ways to solve your problem: use the command bus with the retry middleware (mentioned in that discussion), or manually refresh the ARs on places where these errors arise.

PS: please reopen this issue if the linked discussion doesn't solve the problem!

@brendt brendt closed this as completed Mar 2, 2022
@JustSteveKing
Copy link
Author

@brendt is this part in the v7 documentation at all? It sounds like it would solve the issue - but wouldn't mind reading your recommended approach 😄

@brendt
Copy link
Collaborator

brendt commented Mar 4, 2022

I don't think it is… I added it as a kind of experimental feature, but maybe it's time to add it to the docs now.

Have you tried it out?

@JustSteveKing
Copy link
Author

Not yet @brendt - if it is in the laravel event sourcing couese I could probably figure it out though

@anabeto93
Copy link

@JustSteveKing I'm wondering if you've been able to follow the discussion on #214 and resolved it. I haven't been able to follow through and implement the suggestions but we have worked out a way to make concurrent persists work for us in the mean time. Solution seems hacky and thus haven't bothered to create a PR to this package to have it added.

Found ourselves in a situation where you need the aggregate to persist no matter what. When dealing with transactions (payments), after processing all validation rules and ensuring this is a valid request that needs to be processed by an external microservice, it is bad to throw an error such as CouldNotPersistAggregate. It is not really the fault of the merchant/requestor making the request to the gateway that another process has already persisted the aggregate to a higher version.

Find below the link to a sample repository created, (on a separate branch), to enable concurrent persists. I am really hoping it helps someone and or someone improves upon it in a way that would also help us very much.

https://github.com/anabeto93/spatie-es-auto-discovery/pull/new/feature/allow-concurrent-persists

Helper function here is allowConcurrentPersists() and the tests in AllowsConcurrentPersistsTest might help explain the challenge.

When stress testing the current implementation of the gateway with event-sourcing using locustio, the majority of the errors that occur apart from http 429 mostly has to do with CouldNotPersistAggregate. Still playing around with $tries value to settle between a higher value (slower response times) or lower value (faster response times but with more CouldNotPersistAggregate errors).

@JustSteveKing
Copy link
Author

Hey @anabeto93 I haven't tried this recently if I am honest, however I am about to start another event sourcing project soon - so will be able to see where this is in terms of ability to achieve.

I had issues following the discussion if I am honest, as the docs did not have anything about actually using the command bus. I will dig into this and see what I can figure out

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants