Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
65 changes: 52 additions & 13 deletions .github/workflows/bdd-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,10 @@ jobs:

# Single matrixed job replaces the 21 nearly-identical per-language /
# per-suite jobs that used to live here. Each suite differs only in
# the cargo command it runs; share everything else.
#
# Why no `nick-fields/retry` around the cargo step: the legacy jobs
# wrapped `cargo test` in max_attempts: 2, which masks intermittent
# BDD failures rather than surfaces them. Retry stays on the network
# `docker pull` step, where it covers GHCR manifest visibility lag
# right after a fresh push (the typical real-world flake).
# the cargo command it runs; share everything else. See the cargo
# step below for the retry policy and why three attempts beat two
# on the dominant GHA flake families (DNS to crates.io, timing-
# sensitive lifecycle scenarios).
bdd-tests:
name: 'BDD: ${{ matrix.suite.name }}'
needs: prepare-tests
Expand All @@ -222,6 +219,12 @@ jobs:
packages: read
strategy:
fail-fast: false
# GitHub-hosted runners share CPU under heavy matrix fan-out, and
# the timing-sensitive scenarios (sleep-based retain/lifetime
# waits, SCRAM passthrough reconnect) lose their margin when 20+
# BDD jobs run in parallel. Cap concurrency so each suite gets a
# less contested runner.
max-parallel: 4
matrix:
suite:
- { name: "Go", cargo: "test --test bdd -- --tags @go" }
Expand All @@ -245,6 +248,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 All @@ -269,9 +273,44 @@ jobs:
command: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare-tests.outputs.image-tag }}

- name: Run BDD suite (${{ matrix.suite.name }})
run: |
docker run --rm \
-v ${{ github.workspace }}:/workspace \
-w /workspace \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare-tests.outputs.image-tag }} \
cargo ${{ matrix.suite.cargo }}
# `--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
# re-export the host's systemd-resolved stub at 127.0.0.53.
# When the bridge resolver is wedged for a job, every cargo
# attempt inside the container fails with
# `Could not resolve host: index.crates.io` and stays wedged
# for minutes — both cargo's own retries and an outer step
# retry hit the same dead resolver. Outer retries do not
# rescue that case (we tried 3×30 s in a previous commit and
# the job still drained all three attempts on the wedged
# resolver). Sharing the host's network stack short-circuits
# the bridge resolver entirely; this is safe because each
# matrix entry runs on its own ephemeral runner, so no two
# BDD suites contend for the same loopback ports.
#
# `CARGO_NET_RETRY=10` (default 2) and `CARGO_HTTP_TIMEOUT=60`
# (default 30 s) widen cargo's internal network retry loop for
# the residual case where the host's own resolver works but
# `index.crates.io` itself rate-limits or 5xx's mid-fetch.
#
# The outer 2-attempt retry remains for the timing-sensitive
# BDD flake family (SCRAM passthrough reconnect after retain,
# sleep-based lifecycle waits) that occasionally loses its
# margin under cross-job contention. It is not a network
# safety net — that responsibility moved to `--network=host`
# plus the cargo env vars above.
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 2
retry_wait_seconds: 5
command: |
docker run --rm \
--network=host \
-e CARGO_NET_RETRY=10 \
-e CARGO_HTTP_TIMEOUT=60 \
-v ${{ github.workspace }}:/workspace \
-w /workspace \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.prepare-tests.outputs.image-tag }} \
cargo ${{ matrix.suite.cargo }}
19 changes: 18 additions & 1 deletion .github/workflows/dashboard-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,24 @@ jobs:
> grafana/demo/grafana/provisioning/dashboards/pg_doorman.json

- name: Run smoke + ground-truth against grafana/demo
run: make dashboard-validate-ci
# Demo TPS warms up at different speed across shared GitHub
# runners, so a single attempt occasionally hits a panel that
# has not yet populated its rate window. Retry twice with a
# cleanup tear-down between attempts before failing the job.
run: |
set -e
attempt=1
max_attempts=3
until make dashboard-validate-ci; do
if [ "$attempt" -ge "$max_attempts" ]; then
echo "dashboard-validate-ci failed after $attempt attempts" >&2
exit 1
fi
echo "dashboard-validate-ci attempt $attempt failed; tearing down and retrying" >&2
(cd grafana/demo && docker compose -f docker-compose.yml -f docker-compose.ci.yml down -v) || true
sleep 5
attempt=$((attempt + 1))
done

- name: Collect demo logs on failure
if: failure()
Expand Down
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.idea
.junie
.claude
/target
target/
.DS_Store
.direnv
.pre-commit-config.yaml
Expand Down Expand Up @@ -57,6 +57,12 @@ flamegraph-output/
# Superpowers brainstorm session workdir (VC mockups, server state)
.superpowers/

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

# Internal planning notes (private session artifacts)
/docs/internal-plans/

# 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
6 changes: 5 additions & 1 deletion documentation/en/src/authentication/auth-query.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ pools:
cache_failure_ttl: "30s"
```

The query must return a column named `passwd` or `password` containing the MD5 or SCRAM hash. Extra columns are ignored.
The query must return a column named `passwd` or `password` containing
the MD5 or SCRAM hash. Extra columns are ignored except for
`startup_parameters`. In passthrough mode, pg_doorman reads that column
as a `text` JSON object with per-user PostgreSQL startup parameters.
Dedicated mode ignores the column and logs a warning.

`user` and `password` are the credentials PgDoorman uses to run the lookup query. They must have permission to read the credential column. Either grant access to a custom view (recommended) or use a user in `pg_read_server_files` group.

Expand Down
84 changes: 83 additions & 1 deletion documentation/en/src/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,87 @@
# Changelog

### 3.9.0

Per-pool PostgreSQL startup parameters. pg_doorman can now add
configured GUCs to each backend `StartupMessage`. Values apply in
three layers: `general.startup_parameters`, `pools.<name>.startup_parameters`,
and the optional `startup_parameters` column returned by passthrough
`auth_query`.

PostgreSQL stores these values as the session reset defaults, so
client-side `RESET ALL` and `DISCARD ALL` return to the configured value.
This gives one pool a different `plan_cache_mode`, `statement_timeout`,
`work_mem`, or `idle_in_transaction_session_timeout` without changing
`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
are applied in order. Later layers override earlier ones per key.
- Dedicated `auth_query` mode uses a shared `server_user`, so
pg_doorman ignores the per-user column there and logs one warning per
pool and username.
- A reload that changes startup parameters recycles the affected pools.
Idle backends with the old reset defaults are not reused.

#### 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 startup-parameter
budget of `MAX_STARTUP_PACKET_LENGTH - 512` bytes.
- The resolved parameter set is checked before each backend startup
against PG's 10 000-byte `MAX_STARTUP_PACKET_LENGTH`. If only the
auth_query layer overflows the packet, pg_doorman drops that layer and
keeps the general/pool baseline. If the baseline itself does not fit,
pg_doorman skips all configured keys for that spawn and logs the
byte counts.

#### Behaviour on PG-side rejection

- If PostgreSQL rejects a configured startup parameter at backend
startup, pg_doorman returns PostgreSQL's `ErrorResponse` to the
client unchanged. pg_doorman does not retry without the key and does
not disable the key automatically for the pool. Fix the parameter in
the config; until then, backend startup for that pool fails with
PostgreSQL's own SQLSTATE and message.
- SQLSTATEs with the `57P` prefix (server unavailable) keep mapping to
`ServerUnavailableError` first so the Patroni-assisted fallback
path can route around the failed node before the startup-parameter
log line fires.
- The configured parameter wins over the client sync path:
even if the client connect string carries an `application_name`
(or another tracked GUC like `TimeZone`), the per-checkout
`sync_parameters` call no longer overrides the configured value on
the backend. That default stands until an
explicit `SET` statement on the client session changes it.

#### RELOAD coherence

- A SIGHUP that changes `general.startup_parameters` drains pools that
inherit that baseline. The per-pool config hash includes the general
startup map, and carried-over dynamic `auth_query` pools are recycled
when the baseline changes.

#### Observability

- `pg_doorman_backend_startup_parameter_errors_total{pool, sqlstate}`
counts backend startups PostgreSQL rejected because of an
configured startup parameter. The failing parameter name and username are
written to the warning log line, not to metric labels.
- `SHOW STARTUP_PARAMETERS` (admin SQL console) lists the per-pool
resolved parameters with the source of each value. `psql` tab
completion on `SHOW <TAB>` now includes the command.
- The Web UI pool detail page shows the same rows in a "Startup
parameters (configured)" section, driven by the new
`startup_parameters[]` field on `/api/pools`.

See [PostgreSQL startup parameters](tutorials/startup-parameters.md)
for the configuration walkthrough, plus [General Settings](reference/general.md)
and [Pool Settings](reference/pool.md) for the full parameter list.

### 3.8.5

The web console now accepts JWTs issued by an external SSO proxy
Expand Down Expand Up @@ -108,7 +190,7 @@ live in [`guides/web-ui.md`](guides/web-ui.md).
**Built-in operator dashboard.** pg_doorman exposes a single-page
diagnostic console on the same port as `/metrics`, served from
inside the binary and gated on `[web].ui = true` plus a non-default
`admin_password`. Reaching the same view through the existing psql
`admin_password`. Getting comparable detail from the existing psql
admin console means running `SHOW POOLS`, `SHOW CLIENTS`,
`SHOW STATS` and friends in a loop, computing rates by hand between
two snapshots, and joining the rows mentally. The dashboard does
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) |
| Configured PostgreSQL GUCs in backend `StartupMessage` per pool | Yes (`startup_parameters`, applied as `general` → pool → passthrough `auth_query`; client `RESET ALL` / `DISCARD ALL` returns to those values; PG startup errors reach the client unchanged) | No equivalent configured defaults; selected client startup parameters can be tracked or ignored | No (`maintain_params` preserves client-side parameters across rebind; no configured GUCs) |

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

Expand Down
3 changes: 2 additions & 1 deletion documentation/en/src/guides/web-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ proxy:
| `sso_allowed_users` | Allowlist on the `preferred_username` (or `sub`) claim. `["*"]` accepts every valid JWT; a literal list restricts access to those usernames. | `["*"]` |
| `sso_groups_claim` | Name of the JWT claim that carries the user's group memberships. Read together with `sso_admin_groups`. | `"groups"` |
| `sso_admin_groups` | Group names that promote an SSO user to `Admin`. Empty keeps every SSO login at the read-only `Sso` role. | `[]` |
| `trusted_proxies` | CIDR ranges trusted to set `X-Forwarded-For` / `Forwarded`. Empty trusts only the listener's own peer. See [Access log](#access-log). | `[]` |
| `sso_require_https` | Reject Bearer/cookie/query SSO credentials presented over plain HTTP. The listener treats a request as secure only when the TCP peer is in `trusted_proxies` and `X-Forwarded-Proto: https` is forwarded. Defaults to off so SSO keeps working through a TLS-terminating proxy that reaches pg_doorman over a private HTTP leg. | `false` |
| `trusted_proxies` | CIDR ranges trusted to set `X-Forwarded-For` / `Forwarded` / `X-Forwarded-Proto`. Empty trusts only the listener's own peer. See [Access log](#access-log). | `[]` |

### Promoting SSO users to Admin via group claim

Expand Down
16 changes: 15 additions & 1 deletion documentation/en/src/observability/admin-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Admin commands are read with `SHOW <subcommand>` or executed with bare verbs (`P
| `SHOW LISTS` | Counts by category (databases, users, pools, clients, servers). |
| `SHOW USERS` | List of users and their pool modes. |
| `SHOW AUTH_QUERY` | `auth_query` cache hit/miss/refetch rates, auth success/failure, executor errors, dynamic pool counts. |
| `SHOW STARTUP_PARAMETERS` | Resolved `startup_parameters` per pool: parameter, value, source, and application state. |
| `SHOW SOCKETS` | TCP and Unix socket counts by state (Linux only — reads `/proc/net/`). |
| `SHOW LOG_LEVEL` | Current log level. |
| `SHOW VERSION` | PgDoorman version. |
Expand Down Expand Up @@ -68,6 +69,19 @@ mydb | app | 12 | 4 | 0 | 4 | 36 | 0
- `sv_idle` matches free backends; `sv_active` is in-use; `sv_used` is reserved by the coordinator (see below).
- `maxwait` is the longest current wait in seconds. If it grows beyond `query_wait_timeout`, clients get errors.

### `SHOW STARTUP_PARAMETERS`

```
user | database | parameter | value | source | state
app | mydb | statement_timeout | 5s | general | applied
app | mydb | plan_cache_mode | force_custom_plan | pool | applied
```

- `source` shows where the value came from: `general`, `pool`, or
`auth_query`.
- `state` shows whether the next backend `StartupMessage` will carry
the value: `applied`, `dropped_due_to_budget`, or `stale`.

### `SHOW POOL_COORDINATOR`

```
Expand Down Expand Up @@ -109,7 +123,7 @@ Admin connections do not pass through `pg_hba.conf` rules — they go directly t

## Where to next

- [Prometheus reference](../reference/prometheus.md) — same data, machine-readable.
- [Prometheus reference](../reference/prometheus.md) — the metric form of the same state.
- [Pool Coordinator](../concepts/pool-coordinator.md) — what `SHOW POOL_COORDINATOR` is telling you.
- [Pool Pressure](../tutorials/pool-pressure.md) — what `SHOW POOL_SCALING` is telling you.
- [Troubleshooting](../tutorials/troubleshooting.md) — common failure modes and their `SHOW` output.
Loading
Loading