Skip to content

Synchronous Prooph projections use a stale PDO connection after reconnect #703

Description

@lifinsky

Ecotone version(s) affected: PDO Event Sourcing 1.326.1; reproduced on ecotone-dev main at 9a282a2b with Prooph PDO Event Store 1.16.5.

Description

After a database reconnect, a cached Prooph Projection Manager can keep using the previous PDO connection while the Event Store uses the new one. A synchronous event-driven projection then cannot see an event appended inside the current transaction, leaving the read model unchanged.

How to reproduce

  1. Initialize a synchronous event-driven projection and process a command so that the Prooph Projection Manager is cached.
  2. Close the underlying Doctrine DBAL connection, then begin a transaction on the same DBAL connection object.
  3. Send another aggregate command that appends an event and triggers the synchronous projection.
  4. Query the read model before committing.

Using the existing Ticket fixtures, the relevant part is:

$ecotone->initializeProjection(InProgressTicketList::IN_PROGRESS_TICKET_PROJECTION);
$ecotone->sendCommand(new RegisterTicket('123', 'Johnny', 'alert'));

$connection->close();
$connection->beginTransaction();

try {
    $ecotone->sendCommand(new CloseTicket('123'));

    self::assertTrue($connection->isTransactionActive());
    self::assertEquals([], $ecotone->sendQueryWithRouting('getInProgressTickets'));
} finally {
    $connection->rollBack();
}

Expected: the projection reads through the current connection and removes the closed ticket from the read model before commit.

Actual: the ticket remains in the read model because the cached manager reads through the old connection and cannot see the uncommitted event.

The complete regression test and bootstrap are included in #702 as test_synchronous_projection_uses_the_current_transaction_after_reconnecting. The test also checks rollback and a subsequent retry.

Possible Solution

LazyProophEventStore::getEventStore() already detects a changed PDO and rebuilds its inner Event Store. However, LazyProophProjectionManager::getProjectionManager() returns the manager cached for the context without checking whether its Event Store is still current.

Associate each cached manager with the inner Event Store used to create it, and rebuild the manager when that instance changes. Proposed fix: #702.

Context

Reproduced on PHP 8.5.10, Doctrine DBAL 4.4.4 and PostgreSQL 18, using a single primary database connection, not a read replica.

This affects connection reuse in long-lived processes after a reconnect. Gap retries do not resolve this case: an invisible trailing event can produce an empty stream read rather than a detectable position gap. Polling may mask the visibility symptom by reading after commit, but does not repair the stale manager reference.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions