feat: evict connections with open transactions on pool release#3597
feat: evict connections with open transactions on pool release#3597panga wants to merge 7 commits intobrianc:masterfrom
Conversation
| } | ||
| const activeQuery = this._getActiveQuery() | ||
| this._activeQuery = null | ||
| this._txStatus = msg?.status ?? null |
There was a problem hiding this comment.
Is this check just for tests and pg-native? Both should probably be updated to consistently report a status in their readyForQuery events.
There was a problem hiding this comment.
Added integration tests and updated PR description. However pg-native is not supported because this is a protocol level feature.
|
@charmander thanks for the review. I added a log to align with the behavior in other cases. I couldn’t find a clean way to surface an error at that point since Do you think this behavior change should be behind a pool option and disabled by default? |
Though I think how the pool behaves now (doesn't have any concept of the client being in a transaction and will happily rent it out to another caller later) is almost always wrong, I think it risks weird behavioral breaking changes if we change the default behavior in the So for [email protected] it would be To opt into the behavior. Then in 9.x we can make this behavior the default & actually remove the feature flag entirely since I don't see any reason to allow the old, invalid behavior long term.
I agree w/ @panga that |
My suggestion was to throw an error synchronously from Exposing the status as a public property is a good idea for other reasons, though. |
Yeah that makes sense too. I'll work on a PR soon for the |
|
I implemented the recommended changes for the new pool option I'm concerned that throwing errors from const client = await pool.connect();
try {
return await client.query(...);
} catch (err) {
client.release(err);
} finally {
client.release();
}Edit: The above example is not correct, it need to catch the err to avoid double release. try {
...
} catch (err) {
clientErr = err
} finally {
client.release(clientErr)
} |
Yes, that’s normal JavaScript behaviour for error conditions. Note that this would not be an uncatchable exception, and would typically not terminate the process in applications that care about surviving unexpected error conditions.
That pattern is broken and should not be used, but it’s not incompatible with the general approach of throwing an exception as long as the second |
|
@charmander you’re right. The simplified example I shared above is not correct because it can result in a double release. In practice, the implementation should explicitly handle the error path and ensure the resource is released exactly once, typically by passing the error object to If @brianc is aligned, I can add a throw in this case since it’s behind a feature flag, which should avoid introducing breaking changes for the current version. |
Summary
client.getTransactionStatus()method to return the _txStatus valueevictOnOpenTransaction(disabled by default for backwards compatiblity)client.getTransactionStatus()in pg-pool'srelease()method to evict connections not in idle (I) state instead of returning them to the poolBehavior Changes
evictOnOpenTransaction: true, a connection is closed and removed from the pool if it is released with an active transaction (BEGIN without COMMIT/ROLLBACK).Problem
When a client with an active transaction (BEGIN without COMMIT/ROLLBACK) is released back to the pool, the connection is returned in a non-idle transaction state. The next consumer that checks out this connection inherits the open transaction, which can cause:
This is a known as "leaked/poison connection" problem in connection pools. This feature was recently added in Go pgx jackc/pgx#2481
Notes
pg-nativebecause it works at protocol level which we don't have access inlibpq.