fix(ingest): disable psycopg prepared statements behind the pgbouncer pooler#1032
Conversation
… pooler psycopg3 auto-prepares statements as server-side NAMED statements (`_pg3_N`, a per-client counter). Behind the shared-postgres PgBouncer transaction-mode pooler (Deck #424) many clients share backend server connections, so those names collide (`DuplicatePreparedStatement`, or a param-count `ProtocolViolation`). Each aborts its transaction; in transaction pooling an aborted-but-open transaction never releases its server connection, so the tenant's small server pool (default_pool_size=6 / max_db_connections=12) fills with stuck `idle in transaction (aborted)` backends and a new worker's pool init then times out (`psycopg_pool.PoolTimeout`) → CrashLoopBackOff. Set `prepare_threshold=None` on the procrastinate PsycopgConnector (via the pool's per-connection kwargs). Unconditional: harmless connecting direct (a negligible re-parse cost for this poll-heavy workload) and it removes the footgun of pointing a worker at a transaction-mode pooler without it — exactly how #424 regressed (LISTEN/NOTIFY was disabled for the pooler, prepared statements were not). Reproduced + validated from a live tenant pod (8 clients x25 prepared execs): through the pooler 8/8 failed with DuplicatePreparedStatement; with prepare_threshold=None → 0 errors; direct postgres → 0 errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Chris Coutinho <chris@coutinho.io>
|
Claude finished @cbcoutinho's task in 2m 39s —— View job Review complete
Note: SummaryThis is a well-targeted, minimal fix. The root-cause analysis (per-client
Code quality
Test coverage
Bugs / issues foundNone. No security or performance concerns — this only disables an auto-prepare optimization, it doesn't change query semantics, and the negligible per-query re-parse cost is explicitly called out and justified in the PR description. Minor / optional
|
|



Problem
After the Deck #424 rollout repointed tenant ingest workers at the shared-postgres PgBouncer pooler (transaction mode), the fast workers CrashLoopBackOff on:
Root cause (reproduced + validated from a live tenant pod)
psycopg3 auto-prepares statements as server-side NAMED statements (
_pg3_0,_pg3_1… — a per-client counter). Under transaction pooling, many client connections share the same backend server connections, so two clients each create their own_pg3_0on the same backend:Each error aborts the transaction. In transaction pooling, an aborted-but-open transaction never releases its server connection (it sits
idle in transaction (aborted)), so the tenant's small server pool (default_pool_size=6/max_db_connections=12) fills with stuck connections → a new worker's pool-init query can't get a server connection →PoolTimeout→ crashloop.This is why it works direct (each client owns its server connection; names never collide) but fails through the pooler. #424 correctly disabled
LISTEN/NOTIFYfor transaction pooling but missed prepared statements — the second thing psycopg needs behind pgbouncer transaction mode.Reproduction from a pod (8 clients × 25 prepared execs):
DuplicatePreparedStatement)prepare_threshold=NoneFix
Set
prepare_threshold=Noneon the procrastinatePsycopgConnector(via the pool's per-connectionkwargs), so psycopg uses unnamed statements — safe under transaction pooling. Applied unconditionally: it's harmless connecting direct (a negligible re-parse cost for this poll-heavy workload) and it removes the footgun of pointing a worker at a transaction-mode pooler without it.vector/queue/procrastinate.py: new_psycopg_connector()helper used by bothbuild_app_for_url()andget_procrastinate_app().kwargs={"prepare_threshold": None}.Follow-up
Belt-and-suspenders: enable pgbouncer
max_prepared_statementsonshared-postgres-pooler(gitops) so named statements are safe pooler-wide even for other apps.Deck: board 7 #538 (repo:nextcloud-mcp-server), follow-up to #424.
This PR was generated with the help of AI, and reviewed by a Human