Skip to content

Per-user pooler_mode = "transaction" does not enable prepared statement tracking when [general] pooler_mode = "session" (26000 prepared statement does not exist under read/write split) #1536

Description

@ewhauser

Summary

Config::prepared_statements() decides whether to track and rewrite prepared statements by checking only the global [general] pooler_mode. When [general] is session but a user is overridden to pooler_mode = "transaction", that user gets transaction pooling (correct) but no prepared statement tracking (incorrect). With a replica configured, Parse and Bind for the same statement are routed to different servers and every replica-eligible prepared statement fails with 26000.

Affects v0.1.56; the code is unchanged on main (checked at v0.1.58).

Reproduction

pgdog.toml:

[general]
host = "127.0.0.1"
port = 6432
pooler_mode = "session"          # default for other users on this proxy
query_parser = "on"
read_write_split = "include_primary_if_replica_banned"
read_write_strategy = "conservative"

[[databases]]
name = "app"
database_name = "app"
host = "127.0.0.1"
port = 5432
role = "primary"

[[databases]]
name = "app"
database_name = "app"
host = "127.0.0.1"
port = 5433
role = "replica"

users.toml:

[[users]]
name = "app"
database = "app"
password = "app"
pooler_mode = "transaction"      # per-user override

Client (pgx v5, default QueryExecModeCacheStatement):

cfg, _ := pgx.ParseConfig("postgres://app:app@127.0.0.1:6432/app?sslmode=disable")
conn, _ := pgx.ConnectConfig(ctx, cfg)
var n int64
err := conn.QueryRow(ctx, "SELECT id FROM items WHERE id = $1", int64(1)).Scan(&n)
// ERROR: prepared statement "stmtcache_8975a608c669..." does not exist (SQLSTATE 26000)

Fails on the first execution, every time. Writes through the same connection succeed (both Parse and Bind route to the primary).

What happens

log_level = "debug" shows the sequence:

  1. ['P', 'D', 'S'] — the SELECT is parsed, read: true, executed on the replica. The Parse is forwarded with the client's name (stmtcache_…), not renamed to __pgdog_N, and not recorded in the global cache.
  2. ['B', 'E', 'S']ClientRequest::query() looks up stmtcache_… in the global cache, finds nothing, so there is no AST; the router returns Command::default() (read: false) and the batch goes to the primary.
  3. check_prepared() on the server has nothing to inject, so Bind is forwarded as-is and Postgres returns 26000.

Root cause is pgdog-config/src/core.rs:

pub fn prepared_statements(&self) -> PreparedStatementsLevel {
    // Disable prepared statements automatically in session mode
    if self.config.general.pooler_mode == PoolerMode::Session {
        PreparedStatementsLevel::Disabled
    } else {
        self.config.general.prepared_statements
    }
}

Client::buffer() applies this to every client regardless of the pooler mode its cluster actually resolved to (user.pooler_modedatabase.pooler_modegeneral.pooler_mode, already computed in ClusterConfig::new).

Confirmation: changing only [general] pooler_mode to transaction (leaving the per-user overrides as they are) makes the error disappear on the unpatched binary.

Proposed fix

Derive the level from the cluster's resolved pooler mode instead of the global default:

  • Cluster::prepared_statements_level() returns Disabled only when that cluster is session-pooled, otherwise general.prepared_statements.
  • Client::buffer() uses the connected cluster's level, falling back to Config::prepared_statements() only when there is no cluster (admin DB).

I have a patch against v0.1.56 (about 90 lines including a unit test) that I verified on Linux ARM64: with the config above unchanged, the pgx repro passes and a mixed workload went from 100 % replica-read failures to zero 26000 errors; session-mode clients on the same proxy were unaffected. Existing prepared_statements::, extended_anonymous::, and close_parse_global_cache:: tests pass. Happy to open a PR.

Environment

  • PgDog v0.1.56 (release binary), also reproduced with a main-equivalent build
  • Linux aarch64, Postgres 16 primary + streaming replica
  • Client: pgx v5 (QueryExecModeCacheStatement); any driver using named prepared statements should reproduce

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedThe issue is added to our backlog.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions