Skip to content

fix: follow-up for 3.9.1 (startup_parameters) - #248

Merged
vadv merged 18 commits into
feat/startup-parametersfrom
fix/codex-3.9.1-followup
May 13, 2026
Merged

vadv merged 18 commits into
feat/startup-parametersfrom
fix/codex-3.9.1-followup

Conversation

@vadv

@vadv vadv commented May 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Follow-up patch release that closes the protocol-correctness, observability, and operability gaps codex review surfaced against 3.9.0. Built on top of PR #246 (feat/startup-parameters); merge after it. The configuration shape is unchanged.

Protocol correctness (High)

  • H1 — client startup now filters operator-managed GUC names before merging the client StartupMessage so ParameterStatus matches what PostgreSQL will actually use for the session. The backend's sync_parameters already dropped the same keys on checkout; the two views now agree.
  • H2 — auth_query MD5 retry swaps the refetched cache snapshot into the local view before the dynamic pool is built. Without this, a rotated password matched on retry still built backend_auth and the per-user startup_parameters overlay from the stale row.
  • H3 — backend spawn returns ServerStartupParameterRejection (SQLSTATE 54000) when the operator/auth_query cascade does not fit the StartupMessage budget, instead of silently shipping an empty packet or dropping the overlay. Config validation rejects the deterministic general + pool overflow at load time.

Semantics (Medium)

  • M1 — TimeZone/DateStyle canonicalisation is case-insensitive so a client TIMEZONE cannot slip past operator_managed_startup_keys.
  • M2 — create_dynamic_pool fast path compares the incoming overlay hash against the live pool's frozen hash and drops + rebuilds on drift.
  • M3 — the auth_query reader accepts a native json/jsonb column without requiring a ::text cast (falls back to serde_json::Value).
  • M4 — /api/config and /api/pools reveal startup_parameter values only to Admin. SSO is treated like anonymous so a broad sso_allowed_users list cannot leak tenant/audit tags or accidental secrets.
  • M5 — IMMUTABLES lists full flattened keys (general.host, general.port, web.host, web.port). Previous bare-segment matcher never compared positively against the keys the API actually emits.
  • M6 — overlay keys are classified dropped_due_to_budget instead of stale when the auth_query overlay was dropped because of the budget, even if the baseline carries the same key.

Observability (High/Medium)

  • H5 — monitoring/prometheus-rules/startup-parameters.yaml ships critical alerts for PG-side rejection and cascade overflow (both fail every backend spawn), warnings for malformed auth_query columns and the dedicated-mode drop signal.
  • M11 — PgDoormanWebSsoInsecureTransport alert covers the sso_require_https transport-gate regression so a misconfigured trusted_proxies surfaces as its own alert.

Performance

  • P1/P2/P4 — ServerPool precomputes the wire-ready merged map and budget classification once at pool creation. Backend spawns hand out a borrow on the cached Arc<BTreeMap> instead of cloning the map and re-running the validation walk per checkout. The canonicalised set of operator-managed GUC names is built once per pool and shared with the backend startup path through Server::startup. The single warn line announcing over-budget cascades moves to pool construction; per-spawn paths keep the metric increment and return the PG-style 54000 error from H3.
  • P3 — deferred. The current implementation already redacts values from the anonymous DTO; the remaining savings require a wider refactor to share the value cascade between admin and anonymous paths without losing the Stale vs Applied distinction.

Documentation and CI

  • Reserved-key list now mentions role and session_authorization.
  • Russian tutorials gain the missing pg_doorman_startup_parameters_dropped_total bullet and lose the imaginary "startup family" SQLSTATE phrasing.
  • Web UI guide explains how empty trusted_proxies interacts with sso_require_https.
  • AuthGate login copy no longer hard-codes pg_doorman.toml; works for YAML and include-file deployments.

dmitrivasilyev added 10 commits May 13, 2026 08:34
3.9.1 carries the codex-review follow-up for startup_parameters
(correctness, semantics, observability, performance). Raising BDD
fan-out from 4 to 6 cuts wall time on the 22-suite matrix without
measurable flake increase on the timing-sensitive sleep-heavy
lifecycle/SCRAM-reconnect suites.
H1: client startup filters operator-managed GUC names before
merging the client StartupMessage so ParameterStatus matches what
PG will actually use for the session.

H2: auth_query MD5 retry swaps the refetched cache entry into the
local snapshot. Without it, a rotated password matched on retry
still built backend_auth and the per-user startup_parameters
overlay from the stale row.

H3: backend spawn fails with ServerStartupParameterRejection
(54000) when the operator/auth_query cascade does not fit the
StartupMessage budget, instead of silently shipping an empty
packet or dropping the auth_query overlay. Config validation
refuses the deterministic general+pool overflow at load time.
M1: case-insensitive canonicalization of tracked GUC names so a
client `TIMEZONE` cannot slip past operator_managed_startup_keys
and overwrite the operator default.

M2: dynamic pool fast path now compares the incoming overlay hash
against the live pool's frozen hash. On drift the pool is dropped
and rebuilt against the current snapshot; without this a
concurrent login could inherit a stale per-user startup_parameters
set after an auth_query row update.

M3: the auth_query startup_parameters reader accepts a native
`json`/`jsonb` column without requiring a `::text` cast. The
fallback uses `serde_json::Value` (enabled via the
`with-serde_json-1` feature on tokio-postgres) and falls back to
the existing string decoder.

M6: when the auth_query overlay is dropped because it would push
the cascade over budget, every key from the overlay is reported
as `dropped_due_to_budget` in the admin/API view. The previous
code classified overlay keys that happened to exist in the
baseline as `stale`, sending operators to chase a RELOAD race
that was not the cause.
M4: startup_parameter values in /api/config and /api/pools are now
visible only to Admin. SSO callers see the same masked view as
anonymous so a broad sso_allowed_users list cannot leak tenant
routing tags, audit identifiers, or accidental secrets through
those endpoints.

M5: the immutable-fields list now matches full flattened keys
(general.host, general.port, web.host, web.port). The previous
bare segments never matched the flattened key paths the UI
receives, so every field was rendered as reloadable even when a
restart was actually required.
…ransport

H5: a new startup-parameters.yaml rule group ships critical alerts
for PG-side rejection of configured startup_parameters and for
cascade overflow (both fail every backend spawn), plus warning
alerts for malformed auth_query columns and the dedicated-mode
drop signal.

M11: web-sso.yaml carries a new PgDoormanWebSsoInsecureTransport
warning so a regression in trusted_proxies / X-Forwarded-Proto
plumbing under sso_require_https surfaces as its own alert instead
of hiding inside the generic rejected-auth signal.
P1: ServerPool now precomputes the wire-ready merge of
base_startup_parameters + per_user_startup_overlay once at pool
creation, stores it as an Arc<BTreeMap<String, String>>, and hands
out a borrow on every spawn/admin lookup. Static pools share the
existing base Arc; dynamic pools clone the BTreeMap once instead
of on every backend create.

P2: Server::startup takes the Arc<HashSet<String>> of operator-
managed key names directly from the pool instead of rebuilding the
canonicalized set per spawn. The backend struct now stores the
same Arc the pool/client paths read.

P4: cache the BudgetDecision next to the resolved map so the
spawn path skips the validation walk on every checkout. The
single warn line that announces over-budget cascades moves to
pool construction; per-spawn paths keep the metric increment and
return the PG-style 54000 error from H3.
M12: reserved keys list now mentions role and session_authorization
in both languages so an operator does not waste time on a config
rejection without a documentation clue.

M13: rewording of the budget-overflow paragraph spells out that
general+pool alone can overflow once merged, that an auth_query
overlay can push a previously fitting cascade over the cap, and
that overflow now surfaces as SQLSTATE 54000 on the client side
instead of a silent strip-down.

M14: Russian observability section gains the
pg_doorman_startup_parameters_dropped_total bullet so operators
reading only RU docs see the pre-wire counter.

M15: trusted_proxies entry warns that an empty list with
sso_require_https=true rejects every SSO request behind a TLS
terminator, and points at the CIDR to add.

L2: Russian SQLSTATE wording drops the imaginary "startup family"
category and names the real codes plus a clear "any other code PG
returned for the StartupMessage rejection".

L3: AuthGate login copy points at the active config file instead
of hard-coding pg_doorman.toml; YAML and include-file deployments
now read truthfully.

L4: the AuthGate panel comment describes what the component
actually does instead of pitching a visual style.

L5: Russian auth_query reference clarifies that the
startup_parameters column may be text, json, or jsonb and that the
content must be a JSON object of per-user parameters.

L6: troubleshooting phrase about the client-side username is
rewritten into idiomatic Russian.
Adds focused test coverage for the most user-visible codex review
fixes without bloating the BDD matrix:

- M1: unit tests in src/server/parameters.rs lock in the case-
  insensitive canonicalisation of TimeZone and DateStyle so a
  future rewrite cannot silently drop the mixed-case mapping.

- M3: a new auth_query_jsonb_fixture and BDD scenario in
  startup-parameters.feature show that an auth_query SELECT
  returning a native jsonb column applies the per-user GUC
  without a ::text cast.

- M5: web-ui.feature exercises /api/config and checks that the
  bind-address fields are flagged restart-required so the SPA
  pill renders correctly.

- H3: a Config::validate unit test pins the merged general+pool
  overflow refusal at config load.
Lists the protocol-correctness, auth_query, Web UI, observability,
performance, and documentation changes that close the codex review
gaps against 3.9.0. The configuration shape is unchanged; operators
get a PG-style 54000 instead of a silent startup_parameters drop,
admins keep startup_parameter visibility while SSO loses it, and
the dynamic-pool fast path no longer inherits a stale per-user
overlay.
Keep the user-visible behaviour, drop the per-codex-item breakdown
(H1/M2/P4 callouts belonged in the PR description, not the
release notes).
@vadv vadv changed the title fix: codex review follow-up for 3.9.1 (startup_parameters) fix: follow-up for 3.9.1 (startup_parameters) May 13, 2026
dmitrivasilyev added 8 commits May 13, 2026 10:06
GH-H1: ServerStats was registered before the budget preflight ran,
so an over-budget cascade left the stats entry stuck in sv_login.
Resolve startup_parameters first; only register stats once the
preflight has accepted the spawn.

GH-M1: the slow-path race re-check in create_dynamic_pool now
compares the overlay hash too. Two concurrent logins after an
auth_query row update were able to reuse a pool the losing thread
built with a stale snapshot until the next TTL or RELOAD.

GH-M4: build_resolved_startup canonicalises every GUC name during
the cascade merge. PostgreSQL GUC names are case-insensitive, so a
pool override of `TimeZone` now wins over a general `timezone`
instead of shipping both rows in StartupMessage and letting backend
merge order pick the survivor. The baseline-only fallback used by
OverlayDroppedBaselineKept and the admin/API read model both see
the same canonical view.
GH-M2: the config-load gate now uses packet_and_body_bytes per user
so a configuration whose body fits the operator budget but whose
user/database/application_name push the full StartupMessage over PG
cap fails pg_doorman -t instead of every backend spawn at runtime.

GH-M3: SHOW STARTUP_PARAMETERS and /api/pools report every
configured key as dropped_due_to_budget while the cascade is
spawn-blocking. Previously the baseline keys still surfaced as
applied even though no StartupMessage was going on the wire after
the H3 fail-close.

DevOps review: /api/config flags general.worker_threads,
general.unix_socket_dir, general.backlog as restart-required so
the SPA pill matches the runtime (these shape the tokio runtime
and listener socket at process start and a SIGHUP cannot rebuild
them).

DevOps review: SHOW CONFIG admin command drops connect_timeout
from its immutables list — connect_timeout is reloadable on SIGHUP
and used to falsely paint as restart-required.
…roup C)

GH-M5: when the dynamic-pool spawn fails with
ServerStartupParameterRejection in passthrough mode, drop the
dynamic pool and invalidate that user's cache entry. Without this
the bad overlay sticks until cache_ttl expires, so every reconnect
keeps hitting the same rejection even after the operator fixes the
auth_query row.

GH-M8: dispatch the auth_query startup_parameters column by
PostgreSQL type. text/varchar continues through the original
string decoder; json/jsonb goes straight through
`serde_json::Value` into a shared validator. The previous fallback
chain decoded text, failed, decoded as Value, re-serialised with
`Value::to_string()`, and re-parsed — the second parse is gone.

DBA review: when the MD5 retry refetches a verifier that is no
longer MD5 (operator switched `password_encryption` mid-flight to
SCRAM), invalidate the cache so the next reconnect hits
`cache.get_or_fetch` and takes the SCRAM branch immediately
instead of waiting for `cache_ttl`.
GH-M6: PgDoormanStartupParameterOverlayOversize used to match both
`auth_query_overlay_oversize` (post-H3, every backend spawn for
that user is rejected) and `auth_query_oversize` (parse-time, the
overlay is dropped but the user still authenticates against
general/pool defaults). The two reasons now have separate alerts
and runbooks so an on-call does not get the spawn-blocking text
for the degraded case.

GH-M7: PgDoormanStartupParameterPgRejection drops from critical to
warning. The metric only carries pool/sqlstate; a single bad
auth_query row for one user can tick the counter without
blocking the rest of the pool, so paging the rotation on a single
increment was over-broad. The runbook now reads "at least one
backend startup failed".

DBA review: the fail-close SQLSTATE switches from 54000 (PG's own
program_limit_exceeded) to 53400 (configuration_limit_exceeded).
The rejection happens inside pg_doorman before any
StartupMessage reaches PG, so the code that belongs to PG-internal
limits was misleading both in client error messages and in
log-search greps.

DBA review: the EN tutorial now matches the code — text, json,
and jsonb columns are all accepted without a `::text` cast.
Wraps the post-review tail. The PR is otherwise complete; these
items came up across the second-pass reviews and were small enough
to land in one commit.

- BDD matrix comment in .github/workflows/bdd-tests.yml now
  documents the steady-state 6-parallel cap and why it does not
  reintroduce the timing-sensitive flakes a 4-cap originally
  guarded against.
- The RU startup_parameters tutorial gains the same
  text/json/jsonb dispatch wording the EN tutorial already
  carries — the two were briefly out of sync.
- Both tutorials note that operator-side edits to per-user
  startup_parameters apply only to new backend connections; the
  next client reconnect picks up the row and rebuilds the dynamic
  pool against the new snapshot.
- Client startup no longer materialises a temporary HashMap when
  filtering out operator-managed GUC names; the filter loops
  directly into ServerParameters::set_param. A connect storm with
  configured startup_parameters saves one allocation per client.
- CacheEntry now carries a precomputed per-user overlay hash, set
  in lockstep with the map via a single set_startup_parameters
  helper. The dynamic-pool fast path consumes the hash through a
  new parameter on create_dynamic_pool instead of re-running the
  sort + SipHash on every login.
- Second-pass DBA review (MED): invalidate the auth_query cache
  before dropping the dynamic pool on
  ServerStartupParameterRejection so a racing reconnect cannot
  rebuild the same pool against the still-cached bad overlay.
…ents

Rewrites the prose touched by the codex follow-up to neutral
technical voice. The behavior in this commit is unchanged.

- Tutorial pages and the changelog drop AI-marker phrasing and
  trim the breadth claims to what the code actually does.
- web-sso.yaml runbooks read like the surrounding auth-query rules
  and avoid the "every new backend for this pool failed"
  superlative the metric labels cannot prove.
- BDD scenarios and the new auth_query_jsonb_fixture lose the
  codex-MED breadcrumbs in comments; the cited file:line callouts
  belonged in the PR description, not the test.
- Source comments in web routes, the client startup filter, and
  the route DTOs lose the change-marker prose for the same reason.
…ade and full-packet preflight

- Cascade merge canonicalises GUC names per layer instead of after
  the raw-key merge. A pool `TimeZone` now correctly wins over a
  general `timezone`; previously both keys survived the exact-string
  merge and the canonical collision was resolved by `BTreeMap`
  iteration order, which let the less specific layer win.
  Implemented as `startup_parameters::cascade_canonical_keys`,
  used by the static, dedicated auth_query, and dynamic pool base
  builds plus the runtime resolver. `auth_query` overlay parse
  canonicalises keys before insertion. `canonicalize_param_name`
  now covers every `TRACKED_PARAMETERS` alias instead of only
  `TimeZone`/`DateStyle`.
- Config-load full StartupMessage preflight now mirrors runtime
  inputs: `application_name` defaults to `"pg_doorman"` (the runtime
  default) instead of the empty string, and the dedicated auth_query
  `server_user` is validated alongside the static users. The split
  helper closes the gap where `pg_doorman -t` accepted a config that
  later failed every backend startup with 53400.
- The auth_query json/jsonb decoder applies the same
  `MAX_OPERATOR_BUDGET` size gate the text decoder enforces. A
  large jsonb row is now dropped as `auth_query_oversize` instead
  of being cached and rejected during backend startup.
- Fallback path returns `ServerStartupParameterRejection`
  unchanged. The preflight is host-independent, so the previous
  whitelist-clear / discovery retry served only to wrap the
  PG-style 53400 in a generic `ConnectError` and wipe a healthy
  whitelist on a configuration bug.
- `PgDoormanStartupParameterDedicatedDrops` widens to a 1h
  increase window with `for: 5m`. The default positive `cache_ttl`
  is 1h, so the counter only ticks once per user per hour; the
  previous 15m window could miss a steady misconfiguration even
  with `for: 15m`.
- The BDD matrix comment is rewritten alongside the other
  editorial cleanups in this PR.
…p I)

- Cascade is now fully case-insensitive across the parameter
  surface, not just for the five tracked ParameterStatus names. The
  canonical key function returns the fixed spelling for tracked GUCs
  and ASCII-lowercase for everything else, so a general `work_mem`
  baseline and a pool `Work_Mem` override collapse to one cascaded
  entry instead of shipping both rows. The same canonical form
  drives operator_managed key set, auth_query overlay, dynamic pool
  drift hash, admin/Web read model, and config validation.
  IntervalStyle joins the tracked set with the spelling PG reports
  back at runtime.
- Config validate now rejects same-layer canonical duplicates with
  a message pointing at both raw keys, instead of letting BTreeMap
  iteration order silently pick one.
- FailureSummary keeps the first ServerStartupParameterRejection
  separately so a mixed-failure fallback round (one PG candidate
  rejecting the cascade plus transport failures on the rest) still
  surfaces the real SQLSTATE to the client instead of the generic
  ConnectError wrapper.
- The auth_query JSON/JSONB path enforces MAX_OPERATOR_BUDGET on
  raw wire bytes through a custom FromSql wrapper before serde_json
  walks the value tree. The text path already had this pre-parse
  gate; the asymmetry that let a large jsonb row be fully decoded
  before rejection is closed.
- Client startup reads the operator-managed key set through a
  focused lookup that returns only the Arc<HashSet<String>> the
  filter needs, instead of cloning the whole ConnectionPool on
  every authenticated connect.
- CacheEntry holds one StartupOverlay snapshot pairing the per-user
  map with its precomputed overlay hash. The two values now move
  together by construction; direct field writes that could drift
  the map and the hash are gone.
- The BDD matrix runs the new @Web-UI suite. The /api/config
  restart-required scenario added in an earlier commit now executes
  in CI.
- Generated auth_query reference docs accept text/json/jsonb and
  call out the custom-domain caveat, matching the tutorial and the
  decoder.
- pool config-load validate uses a distinct display kind for the
  dedicated auth_query.server_user message so operators don't hunt
  the name in pool_config.users.
- Grafana dashboard JSON is regenerated to match the panel-prose
  changes that landed in the editorial pass.
- The 3.9.1 changelog lists upgrade notes for the SQLSTATE switch
  (54000 → 53400) and the severity downgrade of the PG-rejection
  alert.
@vadv
vadv merged commit 0c0c720 into feat/startup-parameters May 13, 2026
12 checks passed
@vadv
vadv deleted the fix/codex-3.9.1-followup branch May 13, 2026 09:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant