Repository navigation
Conversation
With several hosts, a host that hangs instead of refusing used up the whole connect timeout, so later hosts were never tried. connect_timeout limits each address attempt (TCP, TLS, startup/auth and the target_session_attrs check) and moves on to the next address when it expires, like libpq's connect_timeout. timeout still caps the whole call. The value is read from the keyword argument, the DSN, the service file or PGCONNECT_TIMEOUT. A connect_timeout DSN option is no longer sent to the server as a setting. Fixes MagicStack#1155. Fixes MagicStack#516.
lesnik512
marked this pull request as ready for review
October 15, 2026 09:32
This was referenced Oct 15, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1155. Related: #516, #1022, discussion #1071.
With several hosts,
connect()runs all of them under onetimeout. If the first host hangs instead of refusing, for example because a firewall drops its packets or the server accepts TCP and never answers, that one attempt uses up the wholetimeoutand the other hosts are never tried. The host loop inconnect_utils._connectmoves on only afterOSErroror_NextHostSignal, and a hang raises neither. With the reproducer from #1155 on 0.32.0,connect()fails withTimeoutErrorafter 5.0s fortimeout=5and after 80.1s fortimeout=80; the second host is never contacted.This PR adds
connect_timeout, which works like the libpq parameter of the same name: it "applies separately to each host name or IP address". psycopg 3's async connect does the same.Why a separate parameter:
timeoutlets a hung first host use all of it. Turningtimeoutinto a per-host limit would change the meaning of every existing call that passes it, including single-host ones. Splittingtimeoutevenly across hosts doesn't work either: a host that refuses at once leaves its share unused, and one slow but healthy host gets cut off early.timeoutcontext manager in the connection path #1087. Use thetimeoutcontext manager in the connection path #1087 movedconnect()to thetimeoutcontext manager but kept one deadline for all hosts, as @inikolaev pointed out there. In discussion Connect timeout with multiple host configuration #1071 they describe patching asyncpg locally for a zero-downtime upgrade, and downstream projects (for example modern-python/db-retry) wrapconnect()to retry host by host.connect_timeoutwas declined as a libpq setting that asyncpg doesn't use. Since then asyncpg has taken on other libpq connection options with libpq's names, DSN keys and environment variables:target_session_attrs/PGTARGETSESSIONATTRS,service/PGSERVICEFILE,krbsrvname/PGKRBSRVNAME,gsslib/PGGSSLIB,sslnegotiation. Using the libpq name means a DSN that works with psql and psycopg also works here, instead of failing withUndefinedObjectError.connect_timeoutunset, the behaviour is the same as today.Behaviour:
connect_timeoutlimits each address attempt: TCP connect, TLS, startup and authentication, and thetarget_session_attrscheck. When it expires, the attempt is dropped, itsTimeoutErrorbecomeslast_error, and the next address is tried, the same as forOSError. If every address times out,TimeoutErroris raised.timeoutkeeps its meaning and its 60s default as the limit for the whole call, so the worst case ismin(timeout, addresses * connect_timeout).target_session_attrs: keyword argument, thenconnect_timeoutin the DSN, then the service file, thenPGCONNECT_TIMEOUT. Zero or a negative value means no per-host limit, as in libpq. Non-numeric values raiseClientConfigurationError.connect_timeoutin a DSN is no longer sent to the server as a setting. Todaypostgresql://...?connect_timeout=10fails withUndefinedObjectError: unrecognized configuration parameter "connect_timeout"(FR: connect_timeout in Connection URIs #516).create_pool()passes connect kwargs through, socreate_pool(..., connect_timeout=2)works without other changes.Two smaller changes in
_connect:prefer-standby, the fallback used to be picked at random from every connection that had been opened. It now comes only from hosts that answered the attribute check. A host whose check timed out could otherwise still be returned.asyncio.TimeoutErroris not a subclass ofOSError, so it gets its ownexceptclause. It re-raises whenconnect_timeoutis unset, which keeps the old behaviour on those versions.libpq takes whole seconds. Here the value can be fractional, like
timeout.Tests:
TestConnectParams: DSN, keyword argument, environment variable and service file resolution, precedence, zero and negative values, invalid values.TestConnectTimeout: a local server that accepts connections and never replies, listed before the test cluster. Withconnect_timeout=0.5the connection goes to the cluster in well under 5s. Without it,timeout=1raisesTimeoutError. With two silent hosts it raisesTimeoutErrorafter about 1s. A DSN withconnect_timeoutnow connects. Two mock-based tests cover a hang during thetarget_session_attrscheck and theprefer-standbyfallback.async_timeoutpath), against PostgreSQL 18. flake8 and mypy are clean.I did not add a test where the TCP handshake itself hangs (a full listen backlog, or a blackholed address). Making that portable without root is fiddly, and the per-address deadline wraps the whole attempt, so the server that accepts and stays silent goes through the same code.