Skip to content

Adds transaction support - #58

Open
WyriHaximus wants to merge 8 commits into
voryx:masterfrom
WyriHaximus-labs:transaction-support
Open

WyriHaximus wants to merge 8 commits into
voryx:masterfrom
WyriHaximus-labs:transaction-support

Conversation

@WyriHaximus

@WyriHaximus WyriHaximus commented Jun 27, 2026 •

Copy link
Copy Markdown
Contributor

This PR adds support for transactions by reserving a single connection for a transaction. While keeping connection sharing for other queries on non-reserved connections.

It must be noted that when all connections are in use by transactions, all other queries and new transactions have to wait in the queue until a connection becomes available. This could lead to delays in the starting of queries and is a major change in the behaviour of this package. Follow-up work is in place to see if a single connection can handle multiple transactions. But that is out of scope for this PR.

To achieve the above, the following changes have been made to this package:

  • Changed ubuntu-latest to ubuntu-24.04 because ikalnytskyi/action-setup-postgres@v7 errors with sudo: 'apt-key': command not found
  • Fixed two deprecations: implicitly nullable types in Client, Connection, ErrorException, and TestCase
  • Introduced a new class ConnectionPool to handle all connection pooling and reservations, and moved all the code related to that out of Client
  • Added transaction, including an example in example/ to start a transaction and give the user a Transaction to run their queries inside that transaction and commit or rollback in the end
  • Hardened Connection against connection issues, ensuring those are handled correctly
  • Integration/TestCase added cleanup in the setup method so the new transaction integration tests don't mess up the other integration tests

Example

$client->transaction()->flatMap(function (PgAsync\Transaction $tx) {
    return $tx->executeStatement('INSERT INTO channel(name) VALUES ($1)', ['example'])
        ->concat($tx->query('SELECT COUNT(*) AS c FROM channel'))
        ->concat($tx->commit());
})->subscribe(
    function () {
        echo "Committed.\n";
    },
    function ($e) {
        echo "Failed: " . $e->getMessage() . "\n";
    }
);

@WyriHaximus
WyriHaximus force-pushed the transaction-support branch 5 times, most recently from f45996a to 9bfd1e1 Compare July 21, 2026 18:17
@WyriHaximus
WyriHaximus force-pushed the transaction-support branch 19 times, most recently from 5a949b0 to 7397828 Compare October 25, 2026 19:14
@WyriHaximus WyriHaximus changed the title [WIP] Add transaction support Add transaction support Oct 25, 2026
@WyriHaximus
WyriHaximus force-pushed the transaction-support branch 3 times, most recently from 4e9015d to 56b6868 Compare October 25, 2026 20:46
@WyriHaximus WyriHaximus changed the title Add transaction support Adds transaction support Oct 25, 2026
This PR adds support for transactions by reserving a single connection for a transaction. While keeping connection sharing for other queries on non-reserved connections.

It must be noted that when all connections are in use by transactions, all other queries and new transactions have to wait in the queue until a connection becomes available. This could lead to delays in the starting of queries and is a major change in the behaviour of this package. Follow-up work is in place to see if a single connection can handle multiple transactions. But that is out of scope for this PR.

To achieve the above, the following changes have been made to this package:
* Changed `ubuntu-latest` to `ubuntu-24.04` because `ikalnytskyi/action-setup-postgres@v7` errors with `sudo: 'apt-key': command not found`
* Fixed two deprecations: implicitly nullable types in `Client`, `Connection`, `ErrorException`, and `TestCase`
* Introduced a new class `ConnectionPool` to handle all connection pooling and reservations, and moved all the code related to that out of `Client`
* Added `transaction`, including an example in `example/` to start a transaction and give the user a `Transaction` to run their queries inside that transaction and `commit` or `rollback` in the end
* Hardened `Connection` against connection issues, ensuring those are handled correctly
* `Integration/TestCase` added cleanup in the setup method so the new transaction integration tests don't mess up the other integration tests

## Example

```php
$client->transaction()->flatMap(function (PgAsync\Transaction $tx) {
    return $tx->executeStatement('INSERT INTO channel(name) VALUES ($1)', ['example'])
        ->concat($tx->query('SELECT COUNT(*) AS c FROM channel'))
        ->concat($tx->commit());
})->subscribe(
    function () {
        echo "Committed.\n";
    },
    function ($e) {
        echo "Failed: " . $e->getMessage() . "\n";
    }
);
```
@WyriHaximus
WyriHaximus marked this pull request as ready for review October 25, 2026 21:36
@WyriHaximus

Copy link
Copy Markdown
Contributor Author

@mbonneau This is now ready for review

createConnection() adds every connection to the pool, including those that are
reserved for a transaction and not ready yet. The cap check subtracted the pending
reservations from that count, so while connections were being established each
waiting transaction saw room under max_connections and opened another one. A burst of
transactions therefore grew the pool far past the cap.

Compare the plain connection count with the cap, in both the query path and the
transaction waiter path.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@jwage

jwage commented Nov 1, 2026 •

Copy link
Copy Markdown

Thanks for this, we are building on transaction-support (webhook ingestion on a ReactPHP server) and have been running it hard. I read the diff and ran the integration suite against PostgreSQL. Everything below has a fix with tests in two PRs against the WyriHaximus-labs/PgAsync branch, so you can pull them into this PR:

  1. The pool does not respect max_connections while connections are still connecting (tryAcquireQueryConnection() and processConnectionWaiters() subtract the pending reservations from the count, so a burst of transactions opens one connection per waiter). Fix with tests: Count connections that are still connecting against the pool cap WyriHaximus-labs/PgAsync#1 (merged). We saw about 1,500 simultaneous transactions open a connection each.
  2. A waiter that unsubscribes while queued stays in the queue (acquire() returns an empty disposable) and is later handed a connection nobody releases.
  3. A failed COMMIT (for example a deferred constraint) never releases the connection.

Items 2 and 3 are fixed in WyriHaximus-labs#2 waiters are removed on dispose, and a failed COMMIT/ROLLBACK releases the connection (rolling back first if the server still has the transaction open). Integration tests are in tests/Integration/TransactionConnectionReleaseTest.php.

Smaller things I did not change: Connection::start() became public only so the pool can call it, and the new Integration/TestCase::setUp() spins the loop for three 50 ms waits, which is timing based.

WyriHaximus and others added 6 commits November 1, 2026 15:22
Count connections that are still connecting against the pool cap
transaction($work) commits when the work completes, rolls back and rethrows
the original error when it fails, and rolls back when the subscriber
unsubscribes. beginTransaction() is the manual form the README documents.

A waiter that unsubscribes while queued for a connection is removed from the
queue, and a connection handed to a waiter that is already gone is released.
A COMMIT/ROLLBACK that fails no longer keeps the connection reserved.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Release the connection after a failed COMMIT, and drop unsubscribed waiters
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

Successfully merging this pull request may close these issues.

2 participants