Skip to content

feat: per-user/per-database override for client_idle_timeout - #1467

Open
mvidaurre wants to merge 8 commits into
pgdogdev:mainfrom
benandfrank:feat/per-user-client-idle-timeout
Open

feat: per-user/per-database override for client_idle_timeout#1467
mvidaurre wants to merge 8 commits into
pgdogdev:mainfrom
benandfrank:feat/per-user-client-idle-timeout

Conversation

@mvidaurre

@mvidaurre mvidaurre commented Sep 1, 2026

Copy link
Copy Markdown

Why

client_idle_timeout is currently global. That works for ordinary request/response clients, but it also disconnects intentionally quiet sessions such as LISTEN/NOTIFY subscribers. PostgreSQL settings cannot prevent this because PgDog applies the timeout to the frontend socket before PostgreSQL is involved.

This change makes the timeout configurable per user or logical database, so operators can exempt only long-lived listeners without weakening idle-client protection for everyone else.

Fixes #1462.

Configuration and precedence

Resolution follows the same precedence used by other PgDog overrides:

  1. Matching user override
  2. Logical database override
  3. [general].client_idle_timeout

At every level, 0 means disabled for that client (Duration::MAX).

Exempt one user

# users.toml
[[users]]
name = "queue_listener"
database = "production"
password = "..."
client_idle_timeout = 0

Override one logical database

# pgdog.toml
[[databases]]
name = "production"
host = "primary.internal"
port = 5432
client_idle_timeout = 300_000

A logical database may have multiple [[databases]] entries for shards or replicas. The first configured non-None override is used across the logical database. Conflicting configured values produce a warning rather than depending silently on physical-entry ordering.

When user entries overlap through all_databases or databases, the last matching entry that configures client_idle_timeout wins. A later matching entry without this setting does not erase an earlier override; this preserves explicit policy when passthrough authentication appends a bare user entry.

Authenticated admin sessions always use the general timeout because the admin database is virtual and has no user or backend database configuration.

Implementation

  • Adds client_idle_timeout: Option<u64> to User and Database, including database-URL query parameter support.
  • Adds ConfigAndUsers::client_idle_timeout(user, database) to centralize precedence and 0 handling.
  • Resolves timeout policy using the authenticated startup user and database in regular and mirror client paths, while retaining the authenticated admin flag across reloads.
  • Keeps resolved timeouts on each client and tracks the configuration with a Weak<ConfigAndUsers>:
    • normal requests perform only an O(1) configuration identity check;
    • configuration reloads are picked up on the next buffer() invocation; an already pending idle read retains its current deadline until the client loop wakes;
    • old configuration snapshots are not kept alive by idle clients.
  • Regenerates both checked-in JSON schemas.
  • Uses a frontend-only loopback fixture for timeout tests, so they do not initialize backend pools or require PostgreSQL.
  • Documents timeout precedence, reload behavior, and examples in docs/CLIENT_CONNECTION.md and the example configuration files.

Reviewer guide

Area Files What to review
Public configuration pgdog-config/src/users.rs, pgdog-config/src/database.rs, pgdog-config/src/url.rs, .schema/* New optional fields, URL parsing, and generated schemas
Resolution semantics pgdog-config/src/core.rs User/database/general precedence, duplicate physical database handling, conflict warning
Runtime integration pgdog/src/frontend/client/timeouts.rs, pgdog/src/frontend/client/mod.rs Timeout selection and reload-aware caching
Mirror path pgdog/src/backend/pool/connection/mirror/mod.rs Destination user/database resolution
Tests pgdog/src/frontend/client/test/mod.rs, timeout/config unit tests Exemption, precedence, admin reloads, snapshot lifetime, URL parsing, and frontend-only fixture

Compatibility

  • Existing configurations without overrides retain the general timeout.
  • client_idle_in_transaction_timeout and backend idle_timeout behavior are unchanged.
  • This is explicit configuration; it does not attempt to infer active LISTEN registrations automatically.
  • client_idle_timeout = 0 now consistently means disabled at general, database, and user levels.

Validation

  • cargo fmt --all -- --check
  • cargo nextest run --profile dev -p pgdog-config --lib — 124 passed
  • Relevant pgdog timeout/routing tests via cargo nextest — 7 passed
  • cargo clippy -p pgdog-config -p pgdog --all-targets — no warnings introduced by this PR
  • JSON-schema regeneration is clean (cargo run -p pgdog-jsonschema)
  • The branch was test-merged with current main; cargo check -p pgdog-config -p pgdog passed

client_idle_timeout was previously global-only, disconnecting
long-idle LISTEN/NOTIFY subscribers with no way to exempt them.
Add an Option<u64> override on User and Database (users.toml /
pgdog.toml), resolved with the same user -> database -> general
precedence used elsewhere (idle_timeout, statement_timeout). 0
at any level exempts the client from the timeout entirely.

Fixes pgdogdev#1462

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NoNS7QBkAU2apgWGLFuMMp
@CLAassistant

CLAassistant commented Sep 1, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@mvidaurre
mvidaurre marked this pull request as ready for review September 1, 2026 02:28
mvidaurre and others added 5 commits August 31, 2026 22:05
The per-user/per-database override work moved timeout resolution to
ConfigAndUsers::client_idle_timeout(user, database), where 0 means
"disabled". This method was no longer called anywhere but still
treated 0 as "time out immediately", the opposite of the new rule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The per-user/per-database override work made 0 mean "disabled" at
every level, including general.client_idle_timeout (previously 0
meant "time out immediately" via the now-removed
General::client_idle_timeout() method). Document this explicitly,
matching the notes already added to the database/user overrides.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- User-level resolution now takes the last matching entry that actually
  configures the setting, so bare entries appended by passthrough auth
  (or broader later entries without the setting) no longer erase an
  earlier override. Regression test included.
- The virtual admin database is exempt from user/database overrides,
  matching pool construction which never associates users with it.
  Regression test included.
- Extracted Config::database_client_idle_timeout() so first-wins
  resolution for duplicate [[databases]] entries lives in one place,
  and rewrote the Config::check merge as a match without shadowed
  bindings.
- Replaced Weak::upgrade + Arc::ptr_eq on the per-request hot path with
  a plain pointer compare (our own Weak pins the allocation, so no ABA).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace filter_map().last() with rev().find_map() — short-circuits from
the end instead of walking every user entry, and avoids clippy's
double_ended_iterator_last. Drop the redundant config_admin_user() test
helper in favor of Admin::default().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

client_idle_timeout is global-only; no per-user/per-pool override to protect long-idle LISTEN clients

2 participants