You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Connection pool hangs if authentication fails #141
When using a connection pool with tokio_postgres, the connection hangs until a TimedOut error is returned if the password is incorrect. This behaviour is undesirable and should just result in authentication failure.
To re-produce, simply try connect to any database using an incorrect password and the connection will hang until it times out.
I've spent a bit of time looking into this while debugging some TLS connection issues. It is decidedly not a simple fix, unfortunately. There are a few things that I've been doing to make life a little bit easier, however:
If your deployment allows it, set Builder::min_idle to require at least one open connection before considering the Pool ready. Consider testing the connection parameters on startup with a short-lived pool and very low timeout.
Be sure to register an ErrorSink with the method Builder::error_sink which will give you an opportunity to log errors that otherwise would fall off the table.
Also, I'll be taking a look at using the crate bb8_failsafe here for more proactive connection error management.
The touchpoint for the issues is at PoolInner::add_connection here:
This is the code that ultimately calls into the ManageConnection impl to make the connection, and (in the case of an error) keeps trying until it gets one successfully or the pool's connection timeout expires.
The right behavior in the general case is to delay returning any error until the timeout, but it would be great if there were a way to identify particular classes of errors that shouldn't wait for the timeout. It seems like that would require cooperation from the ManageConnection impl, which means it would require a new trait method (say something like fn error_is_fatal(&self, e: Self::Error) -> bool { return false; } which could then be used to exit early. Then we could override the default impl of that trait method where appropriate. Unfortunately for my use-case (bb8-postgres) the tokio-postgres crate does not expose error details in a way that could reasonably be used for this purpose. Long story short, doing this would be a lot of work.
As a side note @djc or anyone else looking more deeply, this call stack is Pool::get => PoolInner::get => PoolInner::make_pooled => PoolInner::spawn_replenishing_approvals raises my eyebrows. In particular, it seems like the waiter ought to be a Result channel so that a root cause can bubble back up. As it is, we lose the original error even in cases where we are actively waiting for the connection, which does not seem to be the intent of spawn_replenishing_approvals.
As a side note @djc or anyone else looking more deeply, this call stack is Pool::get => PoolInner::get => PoolInner::make_pooled => PoolInner::spawn_replenishing_approvals raises my eyebrows. In particular, it seems like the waiter ought to be a Result channel so that a root cause can bubble back up. As it is, we lose the original error even in cases where we are actively waiting for the connection, which does not seem to be the intent of spawn_replenishing_approvals.
That sounds sensible. Want to propose a PR?
Unfortunately for my use-case (bb8-postgres) the tokio-postgres crate does not expose error details in a way that could reasonably be used for this purpose. Long story short, doing this would be a lot of work.
Might be worth an issue against tokio-postgres? I would definitely be open to adding an error_is_fatal() like hook to the ManageConnection trait.
Happy to work something up. Since the waiter is stored in internals::PoolInternals and the error pops up in inner::PoolInner::spawn_start_connections, I think the move is to replace this line that throws to the error_sink
In at least one case a PR was merged that exposed a limited interface to the error kind (rust-postgres/rust-postgres#739) so it's reasonable to think a similar PR would be well received.
Hmm, it's not so easy, I guess. The above strategy works, but only if the user has configured retry_connection(false). If not, in practice we hit the "outer" timeout on the waiter channel here:
After a quick review I don't immediately see a great way to get these to line up that doesn't feel like a hack (i.e. make the outer one "a little bit longer" or some such). The outer timeout can't really be removed, since we're relying on that in the face of pool contention. The inner timeout in the context of the retry loop looks locally correct, and it's hard to picture what would need to change really.
I'll go ahead and submit a PR to bubble up the error for the case of a pool configured with retry_connection(false), since that at least makes a little bit of progress here.
still seeing this on bb8 0.9.1 while investigating svix#1012. tested an incorrect password, refused connection, and plaintext against TLS. all three just return TimedOut through the pool, even with retries disabled.
i have a repro and an experimental fix, but wanted to check here before taking it further. would you be open to something like an opt-in way to get the underlying connection error without changing how retries/timeouts work?
i have a repro and an experimental fix, but wanted to check here before taking it further. would you be open to something like an opt-in way to get the underlying connection error without changing how retries/timeouts work?
That sounds more like a workaround than a principled fix? I'd want to fix this right.
i have a repro and an experimental fix, but wanted to check here before taking it further. would you be open to something like an opt-in way to get the underlying connection error without changing how retries/timeouts work?
That sounds more like a workaround than a principled fix? I'd want to fix this right.
i agree, i tested the error_is_fatal approach you suggested earlier with a local prototype. get() returns the connection error when retries are disabled or the error is fatal, and still retries temporary failures until the deadline.
to get that working, i had get() own the connection attempt. that also means cancelling get() cancels that attempt. would you be open to that change?
Activity
djc commentedon Nov 7, 2022
I'd be happy to review a PR for this, but I don't think I'll be able to work on this myself in the near future.
couchand commentedon Feb 27, 2023
I've spent a bit of time looking into this while debugging some TLS connection issues. It is decidedly not a simple fix, unfortunately. There are a few things that I've been doing to make life a little bit easier, however:
Builder::min_idleto require at least one open connection before considering the Pool ready. Consider testing the connection parameters on startup with a short-lived pool and very low timeout.ErrorSinkwith the methodBuilder::error_sinkwhich will give you an opportunity to log errors that otherwise would fall off the table.bb8_failsafehere for more proactive connection error management.The touchpoint for the issues is at
PoolInner::add_connectionhere:bb8/bb8/src/inner.rs
Lines 199 to 225 in 5737736
ManageConnectionimpl to make the connection, and (in the case of an error) keeps trying until it gets one successfully or the pool's connection timeout expires.The right behavior in the general case is to delay returning any error until the timeout, but it would be great if there were a way to identify particular classes of errors that shouldn't wait for the timeout. It seems like that would require cooperation from the
ManageConnectionimpl, which means it would require a new trait method (say something likefn error_is_fatal(&self, e: Self::Error) -> bool { return false; }which could then be used to exit early. Then we could override the default impl of that trait method where appropriate. Unfortunately for my use-case (bb8-postgres) thetokio-postgrescrate does not expose error details in a way that could reasonably be used for this purpose. Long story short, doing this would be a lot of work.As a side note @djc or anyone else looking more deeply, this call stack is
Pool::get=>PoolInner::get=>PoolInner::make_pooled=>PoolInner::spawn_replenishing_approvalsraises my eyebrows. In particular, it seems like the waiter ought to be a Result channel so that a root cause can bubble back up. As it is, we lose the original error even in cases where we are actively waiting for the connection, which does not seem to be the intent ofspawn_replenishing_approvals.bb8/bb8/src/inner.rs
Lines 138 to 139 in 5737736
djc commentedon Mar 3, 2023
That sounds sensible. Want to propose a PR?
Might be worth an issue against tokio-postgres? I would definitely be open to adding an
error_is_fatal()like hook to theManageConnectiontrait.couchand commentedon Mar 3, 2023
Happy to work something up. Since the waiter is stored in
internals::PoolInternalsand the error pops up ininner::PoolInner::spawn_start_connections, I think the move is to replace this line that throws to theerror_sinkbb8/bb8/src/inner.rs
Line 67 in 5737736
with a call to a new method on
SharedPoolthat looks roughly like:It looks like it has been discussed a few times (rust-postgres/rust-postgres#571, rust-postgres/rust-postgres#583, rust-postgres/rust-postgres#790), though usually in a different context. There's even another project that is trying to do exactly the same thing: https://github.com/palfrey/wait-for-db/blob/c3fb9dbad94f06f91f83d6ef9e4af0b93ef64083/src/pg.rs#L34-L49
In at least one case a PR was merged that exposed a limited interface to the error kind (rust-postgres/rust-postgres#739) so it's reasonable to think a similar PR would be well received.
djc commentedon Mar 13, 2023
Yeah, the
forward_error()approach seems reasonable. Should probably verify allsink()callers (at least inPoolInner) to see if they should use this.couchand commentedon Mar 20, 2023
Hmm, it's not so easy, I guess. The above strategy works, but only if the user has configured
retry_connection(false). If not, in practice we hit the "outer" timeout on the waiter channel here:bb8/bb8/src/inner.rs
Line 142 in 03f95c9
bb8/bb8/src/inner.rs
Line 216 in 03f95c9
After a quick review I don't immediately see a great way to get these to line up that doesn't feel like a hack (i.e. make the outer one "a little bit longer" or some such). The outer timeout can't really be removed, since we're relying on that in the face of pool contention. The inner timeout in the context of the retry loop looks locally correct, and it's hard to picture what would need to change really.
I'll go ahead and submit a PR to bubble up the error for the case of a pool configured with
retry_connection(false), since that at least makes a little bit of progress here.Forward error to waiter before sinking
Forward error to waiter before sinking
dekaf: Settle on `deadpool` for the async connection pooling implemen…
kdcokenny commentedon Sep 30, 2026
still seeing this on bb8 0.9.1 while investigating svix#1012. tested an incorrect password, refused connection, and plaintext against TLS. all three just return
TimedOutthrough the pool, even with retries disabled.i have a repro and an experimental fix, but wanted to check here before taking it further. would you be open to something like an opt-in way to get the underlying connection error without changing how retries/timeouts work?
djc commentedon Oct 1, 2026
That sounds more like a workaround than a principled fix? I'd want to fix this right.
kdcokenny commentedon Oct 1, 2026
i agree, i tested the error_is_fatal approach you suggested earlier with a local prototype. get() returns the connection error when retries are disabled or the error is fatal, and still retries temporary failures until the deadline.
to get that working, i had get() own the connection attempt. that also means cancelling get() cancels that attempt. would you be open to that change?
djc commentedon Oct 1, 2026
If you already have a diff, I can have a look at a PR.
kdcokenny commentedon Oct 1, 2026
yep, here ya go! #321