Skip to content

Add --timeout option and a connect_timeout config default - #1622

Open
DiegoDAF wants to merge 1 commit into
dbcli:mainfrom
DiegoDAF:upstream/connect-timeout
Open

Add --timeout option and a connect_timeout config default#1622
DiegoDAF wants to merge 1 commit into
dbcli:mainfrom
DiegoDAF:upstream/connect-timeout

Conversation

@DiegoDAF

@DiegoDAF DiegoDAF commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Description

pgcli never sets a connection timeout, so it inherits libpq's default of 0,
which means "wait until the operating system gives up on the TCP connection".
That can take minutes with no feedback.

Measured against 192.0.2.1 (TEST-NET, silently drops SYNs):

$ time pgcli "postgresql://u@192.0.2.1:5432/db" -c "select 1"
   ... still waiting after 30s, killed manually

That is a poor default for an interactive client: an unreachable host, a
bastion that is down or a wrong port all look like a hang.

Change

Adds a --timeout command line option and a connect_timeout config value
(default 30 seconds). Precedence, highest first:

  1. --timeout
  2. connect_timeout in the connection string
  3. $PGCONNECT_TIMEOUT
  4. the connect_timeout config value

Points 2 and 3 keep libpq's own ordering, and when only the environment
variable is set, nothing is injected so libpq reads it itself. --timeout 0
is meaningful (wait forever, the previous behaviour) and is not treated as
unset, so nobody is locked out of the old default.

The resolved value is passed as a connection parameter rather than merged into
the dsn, so the user's connection string reaches PGExecute exactly as
written. PGExecute keeps connect_timeout alongside the dsn the same way it
already keeps hostaddr.

Validation

Seven new tests in tests/test_main.py covering every precedence combination:
config default, config value, connection string over config, connection string
over env, env left to libpq, --timeout over everything, and --timeout 0.

Two existing tests (test_pg_service_file, test_application_name_db_uri)
assert the exact PGExecute call and now include the resolved timeout.

Measured end to end against the same unreachable address:

setting time to fail
config default 30 30s
connect_timeout=5 in the connection string 5s
PGCONNECT_TIMEOUT=4 5s
--timeout 2 (with 20 in the string and 9 in the env) 2s

Full suite green locally (2737 passed).

Checklist

  • I've added this contribution to the changelog.rst.

Part of the feature list in discussion #1603: this is item 23.

pgcli never sets a connection timeout, so it inherits libpq's default of 0:
wait until the operating system gives up on the TCP connection. Against an
address that swallows SYNs, pgcli keeps hanging for minutes with no feedback.

Adds a --timeout command line option and a connect_timeout config value
(default 30 seconds). Precedence, highest first:

  1. --timeout
  2. connect_timeout in the connection string
  3. $PGCONNECT_TIMEOUT
  4. the config value

Points 2 and 3 keep libpq's own ordering, and when only the environment
variable is set nothing is injected, so libpq reads it itself. --timeout 0 is
meaningful (wait forever) and is not treated as unset.

The resolved value is passed as a connection parameter rather than merged into
the dsn, so the user's connection string is untouched; PGExecute keeps it
alongside the dsn the same way it keeps hostaddr.

Adds seven tests covering each precedence combination. Two existing tests
asserted the exact PGExecute call and now include the resolved timeout.
DiegoDAF added a commit to DiegoDAF/pgcli.daf that referenced this pull request Aug 19, 2026

@dbaty dbaty left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice addition, thanks!

Comment thread pgcli/main.py
in_dsn = "connect_timeout" in conninfo_to_dict(dsn) if dsn else False
if not in_dsn and "connect_timeout" not in kwargs and not os.environ.get("PGCONNECT_TIMEOUT"):
try:
timeout = int(self.default_connect_timeout)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather see this conversion when setting self.default_connect_timeout in __init__(). That way, all uses of the attribute can be sure that it's an integer (and not garbage). We could use this:

self.default_connect_timeout = c["main"].as_int("default_connect_timeout")

That will (1) convert to an integer; (2) automatically default to 30 (from pgclirc) if the user removed the line from their configuration file; and (3) raise an error if the user configured a non-integer value, which I find useful: I prefer when the software says I have done something stupid, instead of silently ignoring it, and making me search and finally find out that, yes, I did something stupid. ;)

Comment thread pgcli/pgexecute.py
if new_params["dsn"]:
# When using DSN, only keep dsn, password, and hostaddr (for SSH tunnels)
new_params = {k: v for k, v in new_params.items() if k in ("dsn", "password", "hostaddr")}
new_params = {k: v for k, v in new_params.items() if k in ("dsn", "password", "hostaddr", "connect_timeout")}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment above is now out-of-date.

Comment thread tests/test_main.py
assert get_editor() is None


def _effective_connect_timeout(tmpdir, cli_timeout=None, dsn_timeout=None, env=None, cfgval=None):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it works... but a get_connect_timeout(dsn, default, kwargs) helper method would probably be easier to unit-test. What do you think?

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