Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1cc1684
Support configurable backend resets for Greengage
vadv Sep 18, 2026
f795f26
Preserve Sync responses when retiring failed backends
vadv Sep 18, 2026
3d51735
Scope configurable backend reset to the opt-in path
vadv Sep 18, 2026
632c20b
Configure adaptive and always backend cleanup policies
vadv Sep 18, 2026
6674396
ci: run Launchpad publishing only for releases
vadv Sep 18, 2026
f8fc5bb
docs: clarify adaptive cleanup conditions in Russian
vadv Sep 18, 2026
5ab013b
docs: clarify cleanup triggers and cache behavior
vadv Sep 18, 2026
5323200
test: cover cleanup policy at async protocol boundaries
vadv Sep 18, 2026
2172979
docs: simplify pooling and cleanup documentation
vadv Sep 20, 2026
75c7fb2
fix: preserve responses and session isolation during cleanup
vadv Sep 21, 2026
b3f2d7d
fix: preserve backend reuse and test cleanup on Greengage
vadv Sep 21, 2026
d5531c0
fix: avoid rebuilding for missing uncompressed frontend index
vadv Sep 21, 2026
d64ca56
fix: honor pool connect_timeout override for backend cleanup
Oct 6, 2026
cdde2c4
test: pin cleanup on failed client write in pooler check query path
Oct 6, 2026
bca3049
test: move custom cleanup queries to always and count built-in cleanu…
Oct 6, 2026
29d5d50
config: reject a cleanup query paired with the adaptive mode
Oct 6, 2026
084660a
fix: forget untracked startup parameters when a client sends DISCARD ALL
Oct 6, 2026
dc19f68
docs: rewrite cleanup wording and drop comments that restate code
Oct 6, 2026
d4c981e
log: keep the plain startup failure that triggers the sslmode=allow r…
Oct 6, 2026
eb404bf
docs: correct the prepared statement cost of always and split joined …
Oct 6, 2026
a98066c
docs: record the cleanup policy in the changelog and cite the PgBounc…
Oct 6, 2026
133fc73
comment: explain why a failed checkout cleanup is not reported to the…
Oct 6, 2026
9a51fbe
docs: open a 3.12.0 changelog section and lead the example with PgBou…
Oct 6, 2026
1af0c6b
log: report a failed checkin cleanup once
Oct 6, 2026
82cdda3
docs: describe pg_doorman_server_cleanup_total in the metrics reference
Oct 6, 2026
3a17b20
server: name the built-in cleanup path and split the mode gate
Oct 6, 2026
194f59d
tests: pin the off and adaptive halves of the built-in cleanup
Oct 6, 2026
8d4b541
feat: configurable backend cleanup with extended adaptive detection
vadv Oct 7, 2026
4d4bf36
test: pin the prepared cache drop on the checkin DEALLOCATE ALL
vadv Oct 7, 2026
85a5ab1
test: count DEALLOCATE ALL in the rollback-split matrix
vadv Oct 7, 2026
8f45b7c
docs: tie the prepared statement cost of always to DISCARD ALL
vadv Oct 7, 2026
bc56ce3
test: pin the lone advisory lock surviving a checkin
vadv Oct 7, 2026
523c848
docs: retire the backend, not close it, when the RU cleanup batch fails
vadv Oct 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/bdd-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ jobs:
- { name: "Rust part 1", cargo: "test --test bdd -- --tags @rust-1" }
- { name: "Rust part 2", cargo: "test --test bdd -- --tags @rust-2" }
- { name: "Rust part 3", cargo: "test --test bdd -- --tags @rust-3" }
- { name: "Greengage 7 cleanup", cargo: "test --test bdd -- --tags @greengage" }
- { name: "Rust part 4 (sleep-heavy lifecycle)", cargo: "test --test bdd -- --tags @rust-4" }
- { name: "Binary upgrade graceful shutdown", cargo: "test --test bdd -- --tags @binary-upgrade-grac-shutdown" }
- { name: "Fuzz", cargo: "test --test bdd -- --tags @fuzz" }
Expand Down Expand Up @@ -431,7 +432,18 @@ jobs:
retry_wait_seconds: 10
command: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare-tests.outputs.image-tag }}

- name: Run Greengage cleanup BDD
if: matrix.suite.name == 'Greengage 7 cleanup'
env:
REPO: ${{ github.repository }}
IMAGE_TAG: ${{ needs.prepare-tests.outputs.image-tag }}
CARGO_HOME: /workspace/.cargo
# prebuild-bdd populated this cache; avoid bridge-network DNS during tests.
CARGO_NET_OFFLINE: "true"
run: tests/nix/run-tests.sh greengage

- name: Run BDD suite (${{ matrix.suite.name }})
if: matrix.suite.name != 'Greengage 7 cleanup'
# `--network=host` is the load-bearing flag. With the default
# bridge network, the container inherits DNS from the docker
# daemon, which on a GitHub-hosted runner does not always
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/launchpad-publish.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
name: Publish to Launchpad PPA

on:
pull_request:
release:
types: [published]

Expand Down
1 change: 0 additions & 1 deletion build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ use std::path::Path;
fn main() {
let dist = Path::new("frontend/dist");
println!("cargo:rerun-if-changed=frontend/dist");
println!("cargo:rerun-if-changed=frontend/dist/index.html");
walk(dist);
}

Expand Down
46 changes: 46 additions & 0 deletions documentation/en/src/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,51 @@
# Changelog

### 3.12.0

#### Configurable backend cleanup: `cleanup_server_connections` modes and `cleanup_server_query`

`cleanup_server_connections` now accepts `off`, `adaptive` or `always`, in `[general]` and per pool.
Legacy `false` and `true` keep working and mean `off` and `adaptive`.

- `adaptive` (default): built-in cleanup, sent only when pg_doorman saw the session state change.
- `always`: `cleanup_server_query` runs on every checkin of a backend that served a client. The query
is required.
- `off`: no session cleanup. ROLLBACK of an unfinished transaction still runs.

`cleanup_server_query` replaces the built-in `RESET ROLE` / `RESET ALL` / `DEALLOCATE ALL` /
`CLOSE ALL` sequence with your own SQL. A good example is PgBouncer's default `server_reset_query`,
`DISCARD ALL`. Validation checks the pair as a whole, with inherited values where a pool sets nothing.
`always` without a query is an error. A query with an effective `adaptive` mode is an error.

```toml
[general]
cleanup_server_connections = "always"
cleanup_server_query = "DISCARD ALL"
```

A failed cleanup retires the backend instead of returning it to the pool with unknown session state.
In `always` the configured query runs on every checkin. A client-side `RESET` or `DISCARD ALL` does not
suppress it. In `adaptive` such a client reset suppresses the built-in cleanup.
A cleanup query like `DISCARD ALL` also removes prepared statements: they are re-parsed after every
checkin. Workloads that rely on prepared statements should stay in `adaptive`.
`pg_doorman_server_cleanup_total` counts cleanup attempts per pool with `result="ok"` or
`result="error"`. Pool-level `cleanup_server_connections` became optional in the config dump, so
`SHOW` and config dumps omit the field when the pool does not set it.

Adaptive detection covers more session state:

- SQL `PREPARE` → `DEALLOCATE ALL` at checkin. SQL `PREPARE` and an extended-protocol `Parse` share one
server-side statement namespace; previously the next client on the same backend could fail with
`42P05 duplicate_prepared_statement`.
- `LISTEN` → `UNLISTEN *` at checkin. A pooled backend's socket is not read, so queued notifications
were delivered to whichever client checked the backend out next.
- `CREATE TEMP TABLE` → `DISCARD TEMP` at checkin. `SELECT ... INTO TEMP` and `CREATE TEMP TABLE AS SELECT`
complete with the inner query's tag and stay untracked. In transaction mode the backend returns to the pool
after every query. A temp table no longer survives the same client's next query; keep a transaction open or
use session mode to keep it longer.
- Every cleanup batch releases advisory locks with `pg_advisory_unlock_all()`. Previously a session-level
advisory lock outlived the client that took it.

### 3.11.2

#### Talos routes `s2i|`-prefixed clients through a service pool
Expand Down
6 changes: 2 additions & 4 deletions documentation/en/src/comparison.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# PgDoorman vs PgBouncer vs Odyssey

Side-by-side feature matrix for choosing a PostgreSQL connection pooler. Every PgBouncer claim is anchored to its [config reference](https://www.pgbouncer.org/config.html) and [changelog](https://www.pgbouncer.org/changelog.html); every Odyssey claim is anchored to the project's [docs](https://github.com/yandex/odyssey/tree/master/docs).

PgCat is intentionally omitted: its design centre is sharding/load-balancing rather than drop-in replacement of PgBouncer, so a row-by-row comparison is misleading. See the [PgCat repo](https://github.com/postgresml/pgcat) if you need horizontal sharding.
Side-by-side feature matrix for choosing a PostgreSQL connection pooler: PgDoorman, PgBouncer and Odyssey.

For benchmark numbers, see [Benchmarks](benchmarks.md).

Expand Down Expand Up @@ -73,7 +71,7 @@ See [Patroni-assisted fallback](tutorials/patroni-assisted-fallback.md), [`patro
| `min_pool_size` (warm connections) | Yes | No | Yes |
| Prepared statements in transaction mode | Yes (named and anonymous, two-level cache, query interner) | Yes (named, since 1.21, `max_prepared_statements`) | Yes (named, `pool_reserve_prepared_statement`) |
| Anonymous `Parse` cache for performance | Yes (`DOORMAN_N`, reused across clients in a pool) | No (anonymous `Parse` passes through unchanged) | No (named statements required) |
| Smart cleanup on checkin (skip `DEALLOCATE ALL` if cache untouched) | Yes (mutation-tracking `RESET ALL` / `DEALLOCATE ALL` on demand) | No (always `DISCARD ALL` if `server_reset_query` set) | Yes (auto) |
| Session cleanup | [`adaptive` (default), `always`, `off`](reference/pool.md#cleanup_server_connections); custom SQL | `server_reset_query`; transaction mode requires [`server_reset_query_always=1`](https://www.pgbouncer.org/config.html#server_reset_query_always) | [`pool_discard` / `pool_smart_discard`](https://github.com/yandex/odyssey/blob/master/docs/configuration/rules.md#pool_discard) |
| LISTEN / NOTIFY pinning in transaction mode | No | No | Experimental |
| Cross-rule connection cap (`shared_pool`) | No | No | Yes (since 1.5.1) |
| `PAUSE` / `RESUME` / `RECONNECT` admin commands | Yes | Yes | Yes (since 1.4.1) |
Expand Down
62 changes: 22 additions & 40 deletions documentation/en/src/concepts/pool-modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

PgDoorman supports two pool modes: `transaction` and `session`. Set per pool, with optional per-user override.

There is no `statement` mode. Statement pooling rotates the backend after every statement, which forces clients to give up multi-statement transactions and breaks the prepared-statement protocol entirely; PgDoorman invests its tuning (prepared-statement cache, direct handoff, strict-FIFO scheduling) in transaction mode instead. PgBouncer keeps `statement` mode for backward compatibility; Odyssey omits it.
There is no `statement` mode.

## Transaction mode (recommended)

Expand All @@ -12,23 +12,11 @@ pools:
pool_mode: "transaction"
```

A backend connection is held for the duration of a transaction, then returned to the pool on `COMMIT`, `ROLLBACK`, or implicit completion.
Supports:

This is the mode that delivers PgDoorman's connection efficiency: a `pool_size` of 40 can serve thousands of clients as long as transactions are short.

What works in transaction mode (where most poolers fail):

- Prepared statements. PgDoorman caches them per-pool, remaps statement names across backend connections, and replays preparation transparently. Drivers that pin to `unnamed` statement (Go pgx, .NET Npgsql, Python asyncpg) work without configuration.
- Pipelined batches and async `Flush` flow.
- Cancel requests over TLS.
- `LISTEN` / `NOTIFY` — but only inside a transaction. A `LISTEN` issued and then committed releases the backend, and any notifications delivered to it after that go to whichever client checks it out next, not to the original `LISTEN`-er. PgBouncer behaves the same way; if you need cross-transaction `LISTEN`, use session mode for that client.

What does **not** work in transaction mode:

- `SET` and `RESET` outside a transaction. Use session mode for clients that rely on session-level GUC changes (`SET TIME ZONE`, `SET search_path` once per connection).
- Advisory locks held across transactions. Use session mode.
- Cursors held outside transactions (`WITH HOLD`). Use session mode.
- `SET LOCAL` works as expected — it is transaction-scoped.
- Named and anonymous prepared statements.
- Pipelined batches and asynchronous `Flush`.
- Query cancellation over TLS.

## Session mode

Expand All @@ -38,16 +26,11 @@ pools:
pool_mode: "session"
```

A backend connection is held for the duration of the client session. Returned to the pool only when the client disconnects.

Use this when:

- The application uses session-scoped state (`SET search_path`, `SET TIME ZONE`).
- The application uses `WITH HOLD` cursors.
- The application uses advisory locks across transactions.
- You are migrating an unmodified PgBouncer deployment that was using session mode and you want a like-for-like swap.
Use for clients that need:

In session mode, `pool_size` is effectively the maximum number of concurrent clients. Sizing matches PostgreSQL's `max_connections` minus reserves.
- Session parameters (`SET search_path`, `SET TIME ZONE`).
- `LISTEN` subscriptions.
- `WITH HOLD` cursors and advisory locks held across transactions.

## Per-user override

Expand All @@ -71,26 +54,25 @@ Useful when one user (operations tooling, migrations) needs session semantics bu

## Cleanup on checkin

Cleanup in transaction mode is **mutation-tracked**, not unconditional. PgDoorman watches each transaction for `SET`, `PREPARE`, and `DECLARE CURSOR`, and only when the backend returns to the pool with one of those flags set does it issue `RESET ALL`, `DEALLOCATE ALL`, or `CLOSE ALL` respectively. A read-only transaction skips cleanup entirely — that's a measurable win on hot OLTP paths.
The default is `cleanup_server_connections: adaptive` without `cleanup_server_query`: pg_doorman cleans a session only when it sees the session state change, so the pooler's cached prepared statements survive a checkin.

What gets reset when a flag fires:
The built-in cleanup tracks five kinds of session state and answers with cleanup SQL:

- `SET` flag → `RESET ALL` drops session-level GUCs and runs `pg_advisory_unlock_all` implicitly.
- `PREPARE` flag → `DEALLOCATE ALL` drops PostgreSQL-side prepared statements that the driver named explicitly. PgDoorman's own prepared-statement cache survives the reset because it is keyed by query text, not by backend name.
- `DECLARE CURSOR` flag → `CLOSE ALL` drops cursors.
- `SET` → `RESET ALL`.
- `DECLARE` cursor → `CLOSE ALL`.
- SQL `PREPARE` → `DEALLOCATE ALL`. SQL `PREPARE` and an extended-protocol `Parse` share one statement namespace.
- `LISTEN` → `UNLISTEN *`.
- `CREATE TEMP TABLE` → `DISCARD TEMP`.

`DEALLOCATE ALL` and `DISCARD ALL` issued by the client clear that client's prepared-statement cache (so the next `Parse` registers anew). The pool-level shared cache is not affected; other clients keep their entries.
Every cleanup batch also releases advisory locks with `pg_advisory_unlock_all()`.

To opt out of cleanup entirely (for performance, in tightly-controlled deployments):
Not tracked: `SELECT ... INTO TEMP` and `CREATE TEMP TABLE AS SELECT` — they complete with the inner query's tag. Temporary objects created inside functions carry no tag. Use `always` with suitable SQL to release these.

```yaml
pools:
mydb:
pool_mode: "transaction"
cleanup_server_connections: false
```
In transaction pooling the backend returns to the pool after every query. A temporary table lives for one transaction; keep a transaction open across the statements that use it.

`cleanup_server_query` replaces the built-in cleanup and requires `always`: your SQL runs on every checkin of a backend that served a client. It is incompatible with `adaptive`, and validation rejects that pairing. The mode costs the server one extra query per checkin. With `DISCARD ALL`, prepared statements are also removed and re-parsed after every checkin. `off` disables session cleanup. Open transactions are rolled back in every mode.

Only do this if you are sure your application never leaks session state. The mutation-tracked default is already cheap when no mutation happened, so the opt-out is rarely worth the risk.
See the [pool reference](../reference/pool.md#cleanup_server_connections) for the full list of tracked statements, limitations, and PostgreSQL and Greengage examples.

## Reference

Expand Down
15 changes: 15 additions & 0 deletions documentation/en/src/tutorials/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,20 @@ make test-bdd TAGS=@admin-commands
make shell
```

### Greengage

```bash
make test-greengage
```

This starts Greengage 7.5.0 with a coordinator and two primary segments,
then runs `@greengage` in the same Nix environment as the PostgreSQL tests.
The Greengage image is pinned by digest in `tests/nix/greengage.sh`;
ARM64 hosts use amd64 emulation. Each scenario gets a separate database,
and the container is removed after the run. These tests also run in CI.

Select a scenario with `make test-greengage TAGS=@greengage-cleanup-built-in`.

### Debug Mode

Enable debug output with the `DEBUG=1` environment variable:
Expand Down Expand Up @@ -125,6 +139,7 @@ This is useful when:
| `@java` | Java client tests (JDBC) |
| `@php` | PHP client tests (PDO) |
| `@rust` | Rust protocol-level tests |
| `@greengage` | Connection cleanup on a real Greengage cluster |
| `@auth-query` | Auth query authentication tests |
| `@copy-protocol` | COPY protocol tests |
| `@cancel` | Query cancellation tests |
Expand Down
6 changes: 2 additions & 4 deletions documentation/ru/src/comparison.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# PgDoorman vs PgBouncer vs Odyssey

Сравнительная матрица фич для выбора пулера соединений PostgreSQL. Каждое утверждение про PgBouncer привязано к [config reference](https://www.pgbouncer.org/config.html) и [changelog](https://www.pgbouncer.org/changelog.html); каждое утверждение про Odyssey — к [docs](https://github.com/yandex/odyssey/tree/master/docs) проекта.

PgCat намеренно опущен: у него центр тяжести — шардинг и балансировка, а не drop-in замена PgBouncer, поэтому построчное сравнение вводит в заблуждение. Если нужен горизонтальный шардинг, см. [репозиторий PgCat](https://github.com/postgresml/pgcat).
Сравнение возможностей пулеров PostgreSQL: pg_doorman, PgBouncer и Odyssey.

Цифры из бенчмарков — [Бенчмарки](benchmarks.md).

Expand Down Expand Up @@ -73,7 +71,7 @@ PgCat намеренно опущен: у него центр тяжести —
| `min_pool_size` (warm connections) | Да | Нет | Да |
| Prepared statements в transaction mode | Да (именованные и анонимные, двухуровневый кеш, query interner) | Да (именованные, с 1.21, `max_prepared_statements`) | Да (именованные, `pool_reserve_prepared_statement`) |
| Кеш анонимного `Parse` для производительности | Да (`DOORMAN_N`, переиспользование между клиентами пула) | Нет (анонимный `Parse` проходит без изменений) | Нет (требуются именованные prepared statements) |
| Умная очистка при возврате соединения (пропустить `DEALLOCATE ALL`, если кеш не менялся) | Да (`RESET ALL` / `DEALLOCATE ALL` по факту мутаций) | Нет (всегда `DISCARD ALL`, если задан `server_reset_query`) | Да (auto) |
| Очистка сессии | [`adaptive` (по умолчанию), `always`, `off`](reference/pool.md#cleanup_server_connections); свой SQL | `server_reset_query`; в transaction требуется [`server_reset_query_always=1`](https://www.pgbouncer.org/config.html#server_reset_query_always) | [`pool_discard` / `pool_smart_discard`](https://github.com/yandex/odyssey/blob/master/docs/configuration/rules.md#pool_discard) |
| LISTEN / NOTIFY pinning в transaction mode | Нет | Нет | Экспериментально |
| Cross-rule connection cap (`shared_pool`) | Нет | Нет | Да (с 1.5.1) |
| Команды администратора `PAUSE` / `RESUME` / `RECONNECT` | Да | Да | Да (с 1.4.1) |
Expand Down
Loading
Loading