feat(pgbouncer): peer so_reuseport workers to forward cross-worker query cancels#10
Merged
Merged
Conversation
…ery cancels When beyond-pg runs more than one so_reuseport pooler worker (PgbScaler scales 1..N on many-vCPU boxes), a Postgres CancelRequest arrives on a fresh TCP connection the kernel may route to a worker that isn't holding the session. PgBouncer had so_reuseport on but no peering configured, so those misrouted cancels were silently dropped — Ctrl-C / statement-timeout / PQcancel would no-op on a busy box with probability (N-1)/N. Configure PgBouncer peering: each worker gets a stable peer_id, a distinct unix_socket_dir, and a shared [peers] map pre-declaring all workers (1..=cap) so the receiving worker forwards a cancel to the peer that owns the session. The map is pre-declared for the full cap so the scaler never rewrites config as it grows/shrinks; forwarding to an idle (not-running) peer just fails, exactly as a dropped cancel would have before, so pre-declaring is safe. - config.rs: pgbouncer_ini takes a peer_id and emits unix_socket_dir/peer_id/ [peers] when max_workers > 1; single-worker boxes keep the empty unix_socket_dir (no worker can misroute a cancel). pgb_peer_id / pgb_peer_socket_dir / pgbouncer_ini_path helpers; peer_id 1 keeps the canonical pgbouncer.ini. - boot.rs: write one config per possible worker and create the per-worker peer socket dirs (owned by postgres) up front. - supervisor.rs: spawn_pgbouncer selects the per-worker config by stable name. - tests/pgbouncer_peering.rs: e2e proof against real pgbouncer 1.25 — a cancel sent to the wrong worker cancels the query with peering ON and is dropped with peering OFF (control). mise test:peering wires it into CI. Verified empirically against pgbouncer 1.25.2: peering ON -> misrouted cancel cancels the query; peering OFF -> query runs to completion (cancel dropped). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Formatting-only line rewrapping (no logic changes) in files that were dirty in the working tree; bundled in so the branch is fmt-clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Problem
beyond-pgfronts Postgres with PgBouncer and runs 1..Nso_reuseportpooler workers (PgbScalerscales up on many-vCPU boxes).so_reuseportwas enabled, but peering was never configured. Peering is what forwards a query-cancel that the kernel routes to the "wrong" worker to the one actually holding the session.Without it, on a box that has scaled past one worker, a
CancelRequest(Ctrl-C, client statement-timeout,PQcancel) opens a fresh TCP connection that the kernel may route to any worker. If it lands on a worker that doesn't hold the session — probability (N-1)/N — the cancel is silently dropped and the query keeps running.pool_mode = transactiondoesn't help; cancelling a mid-statement query is a normal operation.This is the third component of the ClickHouse "scaling PgBouncer" design (fleet +
so_reuseport+ peering); the first two were already implemented here, only peering was missing.Fix
Configure PgBouncer peering, generated per worker at boot:
peer_id, a distinctunix_socket_dir, and an identical[peers]map pre-declaring every possible worker (1..=max_workers).max_workers == 1, i.e.< 8vCPU) can never misroute a cancel, so peering is omitted andunix_socket_dirstays empty — byte-compatible with today.Changes
src/config.rs—pgbouncer_ini(…, peer_id)emitsunix_socket_dir/peer_id/[peers]whenmax_workers > 1; newpgb_peer_id,pgb_peer_socket_dir,pgbouncer_ini_pathhelpers.peer_id 1keeps the canonical/etc/pgbouncer/pgbouncer.ini.src/boot.rs— writes one config per possible worker and creates each peer socket dir (owned bypostgres) up front.src/supervisor.rs—spawn_pgbouncerselects the per-worker config from the worker's stable name.packer/files/pgbouncer/pgbouncer.ini— drops the hardcoded emptyunix_socket_dir(now generated per worker).tests/pgbouncer_peering.rs+mise test:peering— e2e regression test.Empirical validation
Against real PgBouncer 1.25.2 (the version prod's Ubuntu-noble image and the
beyond-pg-testimage both ship), two workers in front of one Postgres, aCancelRequestfor worker A's session sent deliberately to worker B:pg_sleep(30)runs to completion57014) immediatelytests/pgbouncer_peering.rsencodes both cases (the OFF case is a control proving the test discriminates). Config-key correctness is covered byconfigunit tests; the e2e proves pgbouncer honors those keys.🤖 Generated with Claude Code