Skip to content

breaking: new sqlx::Pool architecture - #3582

Open
abonander wants to merge 37 commits into
mainfrom
ab/pool-changes
Open

abonander wants to merge 37 commits into
mainfrom
ab/pool-changes

Conversation

@abonander

@abonander abonander commented Oct 29, 2024 •

Copy link
Copy Markdown
Collaborator
  • Use a separate waiting queue for new connections.
  • Pool inheritance (used for testing) only steals connect permits, not acquire permits.
  • Spawn connection attempts as their own task so they may complete even if the acquire() call is cancelled.
  • Race opening a new connection with acquiring one from the idle queue.
  • acquire() should now be completely cancel-safe.
  • Separate timeout for connecting.
  • New PoolConnector trait superceding both before_connect (requested but not yet implemented) and after_connect callbacks.
    • Implemented for closures returning Future, albeit with a 'static requirement for the returned Future (instead of BoxFuture).
    • May be updated to use async closures in a future release (hopefully backwards compatible but will require an MSRV bump): https://blog.rust-lang.org/inside-rust/2024/08/09/async-closures-call-for-testing.html
    • Can be used to support high availability, or implement custom backoff or connection throttling schemes (e.g. token bucket).
  • Use usize for all connection counts to get rid of weird inconsistencies.

Breaking Changes

  • Pool::set_connect_options() and get_connect_options() have been removed. Instead, implement the new PoolConnector trait (or use a closure) using something like Arc<RwLock<impl ConnectOptions>>.
  • PoolOptions::after_connect() has been removed. Instead, implement PoolConnector (or use a closure), open a connection and then apply any operations necessary.
  • PoolOptions::min_connections(), PoolOptions::max_connections() and Pool::size() now use usize instead of u32.

Fixes #3513
Fixes #3315
Fixes #3132
Fixes #3117
Fixes #2848

Comment thread sqlx-core/src/pool/connect.rs Outdated
Comment thread sqlx-core/src/rt/mod.rs Outdated
@FSMaxB

FSMaxB commented Oct 29, 2025

Copy link
Copy Markdown
Contributor

Just tested this with #2805 and this PR seems to fix that issue 🎉

@abonander

Copy link
Copy Markdown
Collaborator Author

@FSMaxB there aren't any changes in this PR that I would expect to fix #2805. Can you make sure it's not just a flaky test?

@FSMaxB

FSMaxB commented Oct 29, 2025

Copy link
Copy Markdown
Contributor

Yup, pretty sure it's not flaky. If I test with 0.9.0-alpha.1, I repeatably get between 50 and 80 warning logs about transaction already in progress or there not being any transactions.

If I run it with the ab/pool-changes branch, I get none. You can test it yourself with the reproducer in that issue.

@FSMaxB

FSMaxB commented Oct 29, 2025

Copy link
Copy Markdown
Contributor

The only reason I even checked is this item in your list above:

  • acquire() should now be completely cancel-safe.

@FSMaxB

FSMaxB commented Oct 29, 2025

Copy link
Copy Markdown
Contributor

It's perfectly possible that this PR doesn't fix all races that can lead to the issue in #2805, but it seems to have at least fixed the race that the reproducer is triggering.

@abonander
abonander force-pushed the ab/pool-changes branch 2 times, most recently from 97c3af6 to 0dd92b4 Compare December 25, 2025 07:07
This was referenced Jan 17, 2026
@abonander abonander changed the title WIP pool changes breaking: new sqlx::Pool architecture Mar 6, 2026
@rbtcollins

Copy link
Copy Markdown
Contributor

Should this be added to https://github.com/launchbadge/sqlx/milestone/10 ? I would love to get the security toil reduction this would enable, and hate it if this was missed due being forgotten at some critical time :)

@abonander

Copy link
Copy Markdown
Collaborator Author

This won't be forgotten, but it still needs more work and I don't want to hold the 0.9.0 release much longer.

adriangb added a commit to adriangb/sqlx that referenced this pull request Jul 24, 2026
Fixes transact-rs#4349.

`Floating::return_to_pool` holds the pool's `DecrementSizeGuard` while it
does I/O on the connection: an `after_release` hook, a `ping()` to check
the connection is still usable, or a graceful `close()`. None of those were
bounded.

That matters because the I/O can be on a socket that is dead in a way the
socket cannot report. If the server disappeared without closing the
connection, and the client has nothing left to retransmit, no RST is ever
provoked and reads never complete. The returning task then holds its permit
forever and the pool shrinks by one connection. After `max_connections` of
those, every `acquire()` fails with `PoolTimedOut` and the pool never
recovers, even though the server is back.

Each step now gets `RETURN_TO_POOL_TIMEOUT` (5s, matching the existing
`CLOSE_ON_DROP_TIMEOUT`), and on expiry the connection is dropped via
`close_hard()`, which does no I/O. Cancelling `close()` likewise drops the
connection and releases the permit.

The regression test uses an `after_release` hook that never completes, which
is a deterministic stand-in for a socket that never answers: before this
change it exhausts `acquire_timeout`, after it the pool is usable again once
the bound expires.

I realise transact-rs#3582 reworks this area and includes a timeout of its own; this is
meant for main until that lands.
adriangb added a commit to adriangb/sqlx that referenced this pull request Jul 24, 2026
Fixes transact-rs#4349.

`Floating::return_to_pool` holds the pool's `DecrementSizeGuard` while it
does I/O on the connection: an `after_release` hook, a `ping()` to check
the connection is still usable, or a graceful `close()`. None of those were
bounded.

That matters because the I/O can be on a socket that is dead in a way the
socket cannot report. If the server disappeared without closing the
connection, and the client has nothing left to retransmit, no RST is ever
provoked and reads never complete. The returning task then holds its permit
forever and the pool shrinks by one connection. After `max_connections` of
those, every `acquire()` fails with `PoolTimedOut` and the pool never
recovers, even though the server is back.

Each step now gets `RETURN_TO_POOL_TIMEOUT` (5s, matching the existing
`CLOSE_ON_DROP_TIMEOUT`), and on expiry the connection is dropped via
`close_hard()`, which does no I/O. Cancelling `close()` likewise drops the
connection and releases the permit.

The regression test uses an `after_release` hook that never completes, which
is a deterministic stand-in for a socket that never answers: before this
change it exhausts `acquire_timeout`, after it the pool is usable again once
the bound expires.

I realise transact-rs#3582 reworks this area and includes a timeout of its own; this is
meant for main until that lands.
geoHeil added a commit to geoHeil/sqlx that referenced this pull request Sep 9, 2026
Review pointed out that the `ErrorResponse` closing a failed OAUTHBEARER
exchange says only that authentication failed. Everything an application needs
in order to go and get a token is in the server's status document, which the
driver was throwing away, and a token provider cannot be the place where that
happens because the document never reaches it.

So the document is what comes out now. `Error::OAuth` carries an
`OAuthChallenge`, and a connection with no token asks the server for its
parameters instead of failing locally: an empty `auth` value, which is how
RFC 7628 §4.3 and libpq ask. A missing token and a rejected one therefore end
the same way, with the caller holding the issuer and the scope it needs to mint
a token and dial again. Nothing re-dials inside `establish`, which has no
retries and may be handed a socket by the caller.

`oauth_token_provider` goes with it: it could refresh a token but never
discover one, and keeping a pool supplied with fresh tokens is the job of the
connection callback in transact-rs#3582 rather than of a second mechanism here.
`oauth_token` documents the sequence, including that a token set there is not
refreshed.

`tests/postgres/oauth.rs` covers an accepted token, a rejected one, a
connection with no token, a token that would forge a message, and the
dial-again sequence as documented, against a real PostgreSQL 18. The
`postgres_18_oauth` service compiles a validator module for that, because the
server ships none and refuses to run an OAuth exchange without one. Those
tests are `#[ignore]`d, since the mechanism is the server's choice and no
other service asks for a token; the `postgres-oauth` CI job and `x.py` run
them with `--include-ignored`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

5 participants