Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
9bdf028
chore: ignore docs/superpowers (private session artifacts)
May 11, 2026
b74df6b
feat(config): startup_parameters field on general and pool with valid…
May 11, 2026
b95987b
fix(config): align startup_parameters budget with PG StartupMessage cap
May 11, 2026
320e3c0
feat(protocol): startup() accepts operator-supplied extra parameters
May 11, 2026
7230ced
feat(server, pool): cascade resolver and per-pool quarantine state
May 11, 2026
bc28204
comments: drop spec-decision shorthand from startup_parameters code
May 11, 2026
969e2b6
feat(server): Server::startup wires startup_parameters and quarantine
May 11, 2026
ed3f813
feat(auth_query): per-user startup_parameters via optional JSON column
May 11, 2026
4c95817
feat(pool): resolve startup_parameters per-backend and wire quarantine
May 11, 2026
f49317e
feat(observability): metrics and SHOW POOLS for startup_parameters he…
May 11, 2026
e498575
test(bdd): startup_parameters end-to-end scenarios
May 11, 2026
01c354b
fix(server): preserve 57P SQLSTATE mapping for Patroni fallback
May 11, 2026
7463754
fix(pool): post-cascade size check prevents oversize StartupMessage
May 11, 2026
a2ef85e
fix(server): only quarantine startup parameters pg_doorman actually sent
May 11, 2026
15a5f5f
docs(metrics): correct backend_startup_parameter_errors label list
May 11, 2026
cb015ee
fix(server): reset rejection counter on successful backend startup
May 11, 2026
3647c64
docs(config): describe shipped startup_parameters behaviour
May 11, 2026
7b2ae91
fix(quarantine): hot-reload threshold and TTL knobs on config reload
May 11, 2026
be3bcb4
fix(quarantine): clear gauge on TTL expiry without waiting for new sp…
May 11, 2026
541715a
fix(auth_query): point operator at ::text cast on column-type mismatch
May 11, 2026
3c4d06f
docs: operator tutorial for PostgreSQL startup_parameters (EN + RU)
May 11, 2026
6eb77a3
fix(ci): wait past postgres init-restart in docker-smoke
May 11, 2026
5d27039
release: 3.9.0 (per-pool startup_parameters)
May 11, 2026
ecec40f
ci(bdd): run @startup-parameters scenarios in CI
May 11, 2026
19b7c46
admin/web: surface operator-injected startup_parameters + codex fixes
May 11, 2026
3e09aad
startup_parameters: forward PG errors verbatim; close codex HIGH #2/#…
May 12, 2026
bb95309
fix(bdd): enable log capture in the every-connect-fails scenario
May 12, 2026
c2efe79
perf(startup_parameters): cache pool baseline, single-alloc StartupMe…
May 12, 2026
3aabbfe
fix(metrics): keep backend_startup_parameter_errors_total accurate un…
May 12, 2026
93e384b
fix(startup_parameters): forward PG sqlstate verbatim + counter for p…
May 12, 2026
b70e313
refactor(startup_parameters): explicit ServerPool dependency, closure…
May 12, 2026
c438d6f
feat(metrics): count dedicated_mode startup_parameters drops
May 12, 2026
1a88da2
feat(grafana): startup_parameters row on the dashboard
May 12, 2026
42894c7
fix(pool): pass base_startup_parameters to retain test helper
May 12, 2026
2cb0944
ci(grafana): allow empty Startup Parameters panels in demo smoke
May 12, 2026
2d8340b
docs: tighten startup_parameters changelog, tutorial, and SHOW reference
May 12, 2026
c8df8e2
fix(pool): advance startup hash only after reload commit
May 12, 2026
69b3235
fix(pool): skip TLS retry on operator startup-parameter rejection
May 12, 2026
560fbcc
fix(pool): recycle dynamic pools on pool.startup_parameters reload
May 12, 2026
221bfdf
fix(pool): freeze per-user auth_query overlay at dynamic pool creation
May 12, 2026
db2ada3
fix(web): redact startup_parameter values for anonymous /api/pools
May 12, 2026
176e583
fix(pool): preserve ServerStartupParameterRejection on fallback path
May 12, 2026
46f514d
fix(auth_query): drop dynamic pool when refetch changes per-user overlay
May 12, 2026
a2f5c5f
fix(pool): keep baseline when oversize auth_query overlay would strip it
May 12, 2026
ac43df0
fix(metrics): count silent auth_query startup_parameter drops
May 12, 2026
1aa3fe3
fix(metrics): unify startup_parameters_dropped_total on per-event units
May 12, 2026
b59d724
perf(pool): resolve startup_parameters once per fallback round
May 12, 2026
2c3e8c2
docs(metrics): clarify pool label is the pool name, not user@database
May 12, 2026
963027b
perf(auth_query): share startup_parameters via Arc on cache hit
May 12, 2026
a60eaed
fix(grafana): drop user/database selectors from startup_parameters pa…
May 12, 2026
8394efd
fix(admin/web): cross-check SHOW STARTUP_PARAMETERS against wire-read…
May 12, 2026
03ed3e9
perf: single-pass packet sizing + Arc-shared operator key set
May 12, 2026
26853fa
perf(auth_query): validate JSON entries without per-entry BTreeMap
May 12, 2026
3f03823
ci(bdd): cap BDD matrix at 4 parallel suites
May 12, 2026
40a5822
fix(startup_parameters): reject role and session_authorization
May 12, 2026
a9acb6a
fix(web): redact startup_parameter values in anonymous /api/config
May 12, 2026
f151f9a
fix(auth): preserve ServerStartupParameterRejection through cold auth
May 12, 2026
702f7e4
fix(pool): recycle dedicated auth_query shared pool on parent config …
May 12, 2026
b5b8ec5
fix(pool): split startup_parameter resolver into pure + side-effectin…
May 12, 2026
ae7bba2
fix(server): canonicalize operator_managed_startup_keys for sync_para…
May 12, 2026
b5a9772
fix(auth_query): pass fetched overlay into create_dynamic_pool
May 12, 2026
882f884
ci: retry Grafana smoke and BDD suites on transient failure
May 12, 2026
f240a0f
docs: editorial pass on startup_parameters tutorials, references, and…
May 12, 2026
0280499
feat(web): render startup_parameter value and state in PoolDetail
May 12, 2026
09c41bd
chore(reference): regenerate pg_doorman.toml/yaml after fields.yaml e…
May 12, 2026
802a09f
feat(web): add opt-in HTTPS gate for SSO credentials
May 12, 2026
8332473
feat(web): redesign sign-in surface as a Bloomberg-style console
May 12, 2026
dd09d36
chore(gitignore): ignore nested cargo target dirs and internal-plans/
May 12, 2026
b6463d3
fix(web): allow http sso_proxy_url so SSO works behind a TLS-terminat…
May 12, 2026
e0b0f46
ci(bdd): bump cargo retry to 3 with 30s wait so DNS flakes settle
May 12, 2026
5b77586
ci(bdd): share host network and widen cargo retry so DNS isolation ca…
May 12, 2026
19313b2
ci(bdd): revert outer retry bump — DNS isolation is the real fix, not…
May 12, 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
1 change: 1 addition & 0 deletions .github/workflows/bdd-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ jobs:
- { name: "Patroni-assisted fallback", cargo: "test --test bdd -- --tags @patroni_fallback" }
- { name: "Server TLS", cargo: "test --test bdd -- --tags @server-tls" }
- { name: "TLS migration (vendored OpenSSL)", cargo: "test --features tls-migration --test bdd -- --tags @tls-migration" }
- { name: "Startup parameters", cargo: "test --test bdd -- --tags @startup-parameters" }
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ flamegraph-output/
# Superpowers brainstorm session workdir (VC mockups, server state)
.superpowers/

# Superpowers spec drafts (private session artifacts)
/docs/superpowers/

# Per-session agent handoff scratchpads — not part of public history.
.local/

Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pg_doorman"
version = "3.8.5"
version = "3.9.0"
edition = "2021"
rust-version = "1.87.0"
license = "MIT"
Expand Down
1 change: 1 addition & 0 deletions documentation/en/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
- [Pool Modes](concepts/pool-modes.md)
- [Pool Coordinator](concepts/pool-coordinator.md)
- [Anonymous Parse Caching](tutorials/prepared-statements.md)
- [PostgreSQL startup parameters](tutorials/startup-parameters.md)
- [Pool Pressure (advanced)](tutorials/pool-pressure.md)

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

### 3.9.0

Per-pool injection of arbitrary PostgreSQL configuration parameters
(GUCs) into the backend `StartupMessage`. The map cascades over three
levels — `general.startup_parameters`, per-pool overrides, and an
optional `auth_query` JSON column for per-user values in passthrough
mode — and the resulting values are written to `pg_settings.reset_val`,
so they survive client-side `RESET ALL` and `DISCARD ALL`. Among
mainstream poolers this is unique to pg_doorman: PgBouncer only carries
`client_encoding` / `datestyle` / `timezone` per database via the
connection string, Odyssey's `maintain_params` preserves client-side
parameters but offers no operator-side injection, and PgCat exposes no
equivalent.

The most common use case is forcing `plan_cache_mode = "force_custom_plan"`
on a hot OLTP pool that keeps getting bitten by a sticky generic plan;
the same mechanism pins `statement_timeout`, `work_mem`,
`idle_in_transaction_session_timeout`, or any other GUC that a single
application needs without touching `postgresql.conf`, `ALTER ROLE`, or
`ALTER DATABASE`.

#### Cascade resolution

- `general.startup_parameters`, `pools.<name>.startup_parameters`, and
the optional `startup_parameters` text column on an `auth_query` row
merge per key, with the more specific level winning. Auth_query in
dedicated mode (a shared `server_user`) intentionally ignores the
per-user column and logs a one-time warning per pool and username.
- The merged cascade is resolved lazily on every backend spawn from
the live config snapshot, so a `RELOAD` that only changes
`general.startup_parameters` takes effect on the next backend
without recycling the pool.

#### Validation and protocol safety

- Reserved protocol keys (`user`, `database`, `replication`,
`options`, the `_pq_.*` extension prefix) are refused at config load.
- Keys must match the PG GUC naming shape `[A-Za-z_][A-Za-z0-9_.]*`,
values must not contain null bytes, and each level fits the operator
budget of `MAX_STARTUP_PACKET_LENGTH - 512` bytes.
- The full cascade is rechecked at every spawn against PG's 10 000-byte
`MAX_STARTUP_PACKET_LENGTH`; if the union would overflow, all
operator-supplied keys are dropped for that spawn and the event is
logged, so the connection still completes with PG defaults instead
of failing every client request.

#### Quarantine for keys PG keeps rejecting

- After
`general.startup_parameter_quarantine_threshold` consecutive
rejections of the same key (default `3`), pg_doorman parks the key
for `general.startup_parameter_quarantine_ttl` (default 5 minutes)
and stops sending it on subsequent backend startups.
- Both knobs are hot-reloadable: a SIGHUP that touches only the
threshold or TTL takes effect on every pool without rebuild.
- A successful backend startup resets partial-rejection counters for
every key it accepted, keeping the threshold model "N consecutive
rejections" rather than "N rejections ever".
- The quarantine record is keyed on the failing parameter name parsed
from PG's error message and cross-checked against the map pg_doorman
actually sent, so a server-side `ALTER ROLE SET` rejection for an
unrelated key cannot poison the operator's quarantine.
- SQLSTATE class `57P` keeps mapping to `ServerUnavailableError` and
drives the Patroni-assisted fallback path; the quarantine
observability runs alongside that mapping, not in place of it.

#### Observability

- `pg_doorman_backend_startup_parameter_errors_total{pool,parameter,sqlstate}`
counts every rejection. The failing username is in the corresponding
warn log line; it is deliberately not a label, so a dynamic
`auth_query` pool that mints many roles cannot blow up the series
count.
- `pg_doorman_backend_startup_parameter_quarantined{pool,parameter}`
goes to 1 the moment a key is parked and back to 0 when the TTL
expires, including on idle pools where the metrics collector
reconciles the gauge without waiting for the next backend spawn.
- `SHOW POOLS` exposes a `quarantined_params` text column so operators
can see at a glance which keys a pool is currently dropping.

See [PostgreSQL startup parameters](tutorials/startup-parameters.md)
for the operator walkthrough and the
[reference entry](reference/configuration.md) for the full parameter
list.

### 3.8.5

The web console now accepts JWTs issued by an external SSO proxy
Expand Down
1 change: 1 addition & 0 deletions documentation/en/src/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ See [Patroni-assisted fallback](tutorials/patroni-assisted-fallback.md), [`patro
| 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) |
| Operator-supplied PostgreSQL GUCs injected into backend `StartupMessage` per pool | Yes (`startup_parameters`, three-level cascade `general` → pool → `auth_query` passthrough, survives client `RESET ALL` / `DISCARD ALL`, with per-pool quarantine for keys PG keeps rejecting) | No (only `client_encoding` / `datestyle` / `timezone` per-database in the connection string) | No (`maintain_params` preserves client-side parameters across rebind; no operator-side injection) |

See [Pool Coordinator](concepts/pool-coordinator.md), [Pool pressure](tutorials/pool-pressure.md).

Expand Down
186 changes: 186 additions & 0 deletions documentation/en/src/tutorials/startup-parameters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# PostgreSQL startup parameters

Some operators need a few PostgreSQL configuration parameters to apply
to every backend pg_doorman opens, without touching `postgresql.conf`,
`ALTER ROLE`, or `ALTER DATABASE`. Three cases recur in practice:

- A hot OLTP pool gets bitten by a sticky generic plan after the
`plan_cache_mode = auto` heuristic flips. Switching the whole role
to `force_custom_plan` would affect every workload using that role;
scoping the change to one pool is what you want.
- An application that does not set its own `statement_timeout` or
`idle_in_transaction_session_timeout` and cannot be patched fast
enough. The DBA needs a server-side default that survives the
application's own session resets.
- A single application that should announce a stable
`application_name` regardless of what the connecting driver
negotiates, so `pg_stat_activity` and audit logs stay legible.

`startup_parameters` lets pg_doorman do this from its own config.

## Configuration

The cascade has three levels; the more specific level wins per key:

```toml
[general.startup_parameters]
statement_timeout = "5s"

[pools.checkout.startup_parameters]
plan_cache_mode = "force_custom_plan"
work_mem = "64MB"
```

After `SIGHUP` (or `RELOAD` on the admin console) every new backend
for the `checkout` pool starts with `statement_timeout = 5s`,
`plan_cache_mode = force_custom_plan`, and `work_mem = 64MB`. Other
pools keep `statement_timeout = 5s` from `general` and the PG default
for the rest. Already-open backends are not affected; the change takes
hold as the pool rotates connections.

When `auth_query` runs in passthrough mode (no `server_user`), the
lookup SQL may return an optional `startup_parameters` text column
holding a JSON object. Values from that column override both
`general` and per-pool settings for that user only:

```sql
SELECT
rolpassword AS passwd,
CASE rolname
WHEN 'vip' THEN '{"work_mem":"256MB"}'::text
ELSE NULL::text
END AS startup_parameters
FROM pg_authid
WHERE rolname = $1;
```

The column must serialise as `text`. If the SQL returns `json` or
`jsonb`, add an explicit `::text` cast. pg_doorman reads the column
as `text` and logs a one-time warning per user when the type does
not match.

Dedicated `auth_query` mode (`server_user` set) ignores the per-user
column and logs once per (pool, username): one shared backend serves
many users, so a per-user override cannot apply.

## What pg_doorman does with the values

The merged map is written into the PostgreSQL `StartupMessage` of
every backend pg_doorman opens. PG records each entry as the session
default for that setting (`pg_settings.reset_val` and
`pg_settings.source = 'session'`), so client-side `RESET ALL` and
`DISCARD ALL` restore the operator value rather than discarding it.
Operators get a stable session default without editing
`postgresql.conf` or running `ALTER ROLE`.

The values can be observed from the client:

```text
checkout=> SHOW plan_cache_mode;
plan_cache_mode
-------------------
force_custom_plan

checkout=> SET plan_cache_mode = 'auto'; RESET ALL; SHOW plan_cache_mode;
plan_cache_mode
-------------------
force_custom_plan
```

## Validation

At config load:

- Keys must match PG GUC naming `^[A-Za-z_][A-Za-z0-9_.]*$`. Namespaced
names like `auto_explain.log_min_duration` are accepted; arbitrary
punctuation is not.
- Reserved keys (`user`, `database`, `replication`, `options`, and
anything starting with `_pq_.`) are refused. pg_doorman manages
them itself or PG treats them specially in the StartupMessage.
- Values must not contain null bytes.
- Each level (general or per-pool) must fit within the operator
budget: `MAX_STARTUP_PACKET_LENGTH` (10 000 bytes) minus 512 bytes
reserved for pg_doorman-managed keys.

At every backend spawn pg_doorman re-checks the merged cascade
against the same cap. Two levels that fit individually can together
push past it once `auth_query` adds a third layer; when that happens
pg_doorman drops every operator-supplied key for that one spawn,
logs the byte counts, and lets the backend connect with PG's own
defaults rather than failing every connection attempt.

## Quarantine

PG can reject an operator-supplied parameter at backend startup with
sqlstate `22023` (invalid value), `42704` (undefined object: typo
in the parameter name), or `42501` (insufficient privilege: the
parameter requires a higher role than the backend has). pg_doorman
counts consecutive rejections per `(pool, parameter)`; once the
count reaches `general.startup_parameter_quarantine_threshold`
(default 3), pg_doorman stops sending that key for
`general.startup_parameter_quarantine_ttl` (default 5 minutes).

A successful backend startup resets the counter for every key it
just sent. Three transient rejections do not trigger quarantine if a
successful spawn lands between them; "consecutive" really means
consecutive.

Both knobs are hot-reloaded on `SIGHUP`. Releasing a quarantined
parameter is **TTL-only**: pg_doorman cannot tell whether the
operator has fixed the underlying problem until the TTL expires and
the next backend spawn tries the parameter again.

## Observability

`SHOW POOLS` carries a `quarantined_params` column listing the
currently parked parameters per pool, comma-separated:

```text
database | user | ... | quarantined_params
----------+-------+-----+--------------------
checkout | shop | ... | work_mem,search_path
reports | shop | ... |
```

The same state is on the Prometheus surface:

- `pg_doorman_backend_startup_parameter_errors_total{pool, parameter, sqlstate}`
counts every rejection from PG. Increments once per failed
StartupMessage; the failing username is on the corresponding warn
log line so dynamic `auth_query` pools cannot blow up the series
count.
- `pg_doorman_backend_startup_parameter_quarantined{pool, parameter}`
is `1` while a parameter is parked and flips back to `0` exactly
once when its TTL expires. The gauge does not clear on its own
even after the operator fixes the underlying issue; the next
spawn after TTL expiry is what re-arms it.

A reasonable starting alert is "any non-zero quarantine gauge for
longer than the TTL". If the gauge is set for `2 × ttl`, the
operator-supplied value is still wrong after one retry window and
deserves a look.

## When not to use this

- The application already sets the parameter on every connection.
Putting the same value in `startup_parameters` adds a bookkeeping
surface for no behavioural change.
- Per-transaction tuning (`SET LOCAL`). `startup_parameters` is for
session defaults; transaction-scoped tuning belongs in the
application.
- Anything that needs to depend on which query the application is
running. Startup parameters apply to every transaction on every
backend for the lifetime of that backend; there is no
per-statement variant.

## Reference

- [General Settings](../reference/general.md): `startup_parameters`,
`startup_parameter_quarantine_threshold`,
`startup_parameter_quarantine_ttl`.
- [Pool Settings](../reference/pool.md):
`pools.<name>.startup_parameters`.
- [auth_query](../authentication/auth-query.md): passthrough vs
dedicated modes, where the `startup_parameters` column is read.
- [Admin Commands](../observability/admin-commands.md): `SHOW POOLS`.
- [Prometheus](../reference/prometheus.md): full metric list.
1 change: 1 addition & 0 deletions documentation/ru/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
- [Режимы пула](concepts/pool-modes.md)
- [Координатор пулов](concepts/pool-coordinator.md)
- [Кеш Parse для анонимных prepared statements](tutorials/prepared-statements.md)
- [Startup-параметры PostgreSQL](tutorials/startup-parameters.md)
- [Пул под нагрузкой (продвинутое)](tutorials/pool-pressure.md)

# Высокая доступность
Expand Down
1 change: 1 addition & 0 deletions documentation/ru/src/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ PgCat намеренно опущен: у него центр тяжести —
| LISTEN / NOTIFY pinning в transaction mode | Нет | Нет | Экспериментально |
| Cross-rule connection cap (`shared_pool`) | Нет | Нет | Да (с 1.5.1) |
| Команды администратора `PAUSE` / `RESUME` / `RECONNECT` | Да | Да | Да (с 1.4.1) |
| Внедрение операторских GUC PostgreSQL в `StartupMessage` бэкенда на уровне пула | Да (`startup_parameters`, трёхуровневый каскад `general` → пул → `auth_query` passthrough, значения переживают клиентские `RESET ALL` / `DISCARD ALL`, карантин на ключи, которые PG раз за разом отклоняет) | Нет (только `client_encoding` / `datestyle` / `timezone` в строке подключения на уровне базы) | Нет (`maintain_params` сохраняет параметры клиента при rebind, операторской инжекции нет) |

См. [Координатор пулов](concepts/pool-coordinator.md), [Пул под нагрузкой](tutorials/pool-pressure.md).

Expand Down
Loading
Loading