Skip to content

Add PostgreSQL 17, 18, and 19 support#1144

Merged
dimitri merged 17 commits into
mainfrom
fix/pg17-18-19-support
Jul 15, 2026
Merged

Add PostgreSQL 17, 18, and 19 support#1144
dimitri merged 17 commits into
mainfrom
fix/pg17-18-19-support

Conversation

@dimitri

@dimitri dimitri commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Add Postgres 17, 18, and 19 support

This PR extends pg_auto_failover to compile and run correctly on PostgreSQL 17, 18, and 19, and updates CI to cover all three new versions.

Closes #1117
Closes #1119
Supersedes #1111
Supersedes #1120

Changes

Core compatibility

  • version_compat.h: raise upper bound to PG20 so PG19 compiles
  • Makefile: add PG17–19 to PGVERSIONS; use CITUSTAG=v14.1.0 for PG18/19 (Citus v13 does not support PG18+)

PG17 fix — dbname in primary_conninfo (fixes #1116)

PG17 introduced synchronized_standby_slots for logical replication slot failover. When primary_conninfo lacks a dbname keyword the slot sync worker silently skips slot synchronisation. The fix threads dbname through ReplicationSource so it is always included in the generated primary_conninfo.

PG18 fix — received_tli NULL during WALRCV_CONNECTING

PG18 added a new WALRCV_CONNECTING state to pg_stat_wal_receiver where received_tli is NULL. Querying it unconditionally caused a crash. The fix guards against NULL and falls back to pg_control_checkpoint().timeline_id during that transient state.

PG19 fixes — failover stall and PgStartTime shim

  • received_tli > 0 filter: PG19 changed how WAL receiver state is reported; guard with received_tli > 0 to avoid acting on a stale receiver row, which caused the FSM to stall during failover.
  • Remove the PgStartTime → MyStartTimestamp shim: PG19 dropped PgStartTime; the shim in version_compat.h caused a double-definition error.
  • Fix pidfile race in service restart: atomic write + defensive retry, surfaced by PG19's faster startup.
  • Fix zero-warning policy violations: old-style function definitions and vendored fallthrough macro, flagged by PG19's stricter compiler defaults.

CI updates

  • Dockerfile.base / Makefile.docker: add PG18 and PG19, with CITUSTAG_18=v14.1.0 and CITUSTAG_19=none.
  • ci.yml: extend all build and test matrices to PG18/19; force-rebuild base image when Dockerfile.base changes in a PR; PG19 Citus jobs omitted (no Citus release supports PG19 yet).

Diagnostic spec (excluded from CI)

tests/tap/specs/debug_failover_pg19.pgaf — a focused two-node failover spec used to reproduce and verify the PG19 stall fix locally. It is not included in any CI schedule.

Testing

All existing test suites pass on PG14–17. PG18 passes the full test suite including Citus. PG19 passes all non-Citus tests; Citus jobs are gated with continue-on-error pending upstream Citus PG19 support.

@dimitri dimitri self-assigned this Jul 12, 2026
@dimitri dimitri added the enhancement New feature or request label Jul 12, 2026
@dimitri dimitri changed the title Add PostgreSQL 17, 18, and 19 support Add PostgreSQL 17, 18, and 19 support + graceful keeper shutdown Jul 12, 2026
@dimitri dimitri force-pushed the fix/pg17-18-19-support branch 2 times, most recently from 56dbc0a to 3a367c7 Compare July 13, 2026 02:58
dimitri added 13 commits July 15, 2026 01:52
- Raise version_compat.h upper bound to PG20 (allows PG19 compilation)
- Add PG19 to PGVERSIONS in Makefile; use Citus v14.1.0 for PG18/19
- Extend CI matrices (run-tests.yml, run-pgaftest.yml) to cover PG18/19
  with correct Citus version per release; bump pgaftest builder to PG18
- Fix #1116: thread dbname through ReplicationSource so primary_conninfo
  includes a dbname keyword, required by PG17 synchronized_standby_slots
  for logical replication slot failover
- Guard pg_stat_wal_receiver.received_tli against NULL: PG18 introduced
  WALRCV_CONNECTING state where received_tli is NULL; fall back to
  pg_control_checkpoint() timeline_id during that transient state

Fixes #1116
PG19 requires several API and build system adaptations:

Build system:
- Remove explicit -std=c99 from Makefile.common and monitor/Makefile;
  pg_config --cflags now provides the right standard per version
  (gnu23 for PG19, c99/c11 for older versions)
- Add libnuma-dev to Dockerfile build deps (PG18+ links against libnuma)
- Support CITUSTAG=none in Dockerfile to skip Citus build for PG versions
  not yet supported by any Citus release (PG19)
- Update CI case statements: 18 -> v14.1.0, 19 -> none

Signal handlers (PG19 changed SIGNAL_ARGS to two-argument SA_SIGINFO form):
- Use SIGNAL_ARGS macro in all signal handler signatures
  (signals.c catch_reload/int/term/quit/quit_and_exit, watch.c catch_sigwinch)
- Use PG_SIG_IGN instead of SIG_IGN with pqsignal() in health_check_worker.c
- Add backward-compat PG_SIG_IGN/PG_SIG_DFL macros to version_compat.h
  for builds against PG < 19

Monitor extension (PG19 API changes):
- LWLockNewTrancheId() now takes the tranche name; LWLockRegisterTranche()
  is removed — add LWLockNewTrancheIdCompat() wrapper in version_compat.h
- ShmemInitHash() dropped the separate init/max size params — add
  ShmemInitHashCompat() wrapper
- WAIT_EVENT_CLIENT_READ moved to utils/wait_event.h; include explicitly
- storage/lock.h and storage/lmgr.h no longer pulled in transitively;
  add explicit includes to metadata.c and health_check_worker.c
- PgStartTime renamed to MyStartTimestamp — compat define in version_compat.h
- utils/timestamp.h no longer included transitively — add in version_compat.h
- pg_fallthrough macro used instead of /* FALLTHROUGH */ comment;
  add backward-compat pg_fallthrough define in version_compat.h for PG < 12
- Fix checkPgAutoFailoverVersion() old-style function definition

Regression tests:
- PG19 changed pg_lsn display format (e.g. 0/0 -> 0/00000000);
  add expected/pg19/expected/ with adjusted output for monitor and
  fast_forward tests; Makefile auto-selects this dir for PG >= 19

All 9 installcheck tests pass on PG16, PG17, PG18, and PG19.
supervisor_update_pidfile() was rewriting the pidfile using O_TRUNC,
which zeros the file before writing the new content.  If cli_do_service
restart read the pidfile in that brief window it saw an empty file and
called log_fatal immediately.

Primary fix: write to supervisor.pidfile.tmp then rename(2) into place.
POSIX rename is atomic so readers always see a complete file.

Defensive fix: retry supervisor_find_service_pid up to 10 times with
10ms sleeps in cli_do_service_restart before treating the lookup as a
fatal error.
…state

The previous fix for PG19's WALRCV_CONNECTING state used:

  WHERE status <> 'connecting'

to exclude the connecting row and fall back to pg_control_checkpoint().
This is wrong when the WAL receiver restarts after having already
streamed: PG19 retains receivedTLI in shared memory across WAL receiver
restarts (WalRcvDie() does not reset it), so a restarting receiver in
WALRCV_CONNECTING state still has received_tli=3 from the previous
session, but the WHERE filter drops that row and the fallback returns
timeline_id=2 from the last restartpoint.

The monitor then sees node2 reporting TLI=2 while node1's last report
was TLI=3, which prevents the automatic failover from completing within
the 90-second timeout.

Fix: filter on received_tli > 0 instead of status <> 'connecting'.
This correctly handles both cases:
- First-ever WAL receiver (shared memory zeroed at startup):
  received_tli=0 → filtered → falls back to pg_control_checkpoint()
- WAL receiver restart after streaming TL3:
  received_tli=3 retained from previous session → not filtered → returns 3
PgStartTime (the postmaster startup time) still exists in PG19 via
utils/timestamp.h.  The earlier shim incorrectly aliased it to
MyStartTimestamp, which is the *per-backend* start time set by
InitProcessGlobals() in every new process.

With the shim in place, NodeIsUnhealthy() compared health-check
timestamps against 'when did this backend connection start?'  A backend
forked to handle a node_active call is always younger than the monitored
healthCheckTime, so the condition

    TimestampDifferenceExceeds(PgStartTime, node->healthCheckTime, 0)

was permanently false in PG19 — the monitor marked an unhealthy primary
but the FSM never advanced to prepare_promotion because NodeIsUnhealthy
kept returning false.  The startup-grace-period check was also broken by
the same substitution.

Fix: delete the shim.  PgStartTime remains valid in PG19 and refers to
the same postmaster start time as in earlier releases.
Documents the investigation technique used to root-cause the PG19
failover stall: polling pg_stat_wal_receiver, pg_stat_activity, and
pgautofailover.node every few seconds to observe monitor state in real
time.  Useful as a reusable template for debugging future FSM stalls.

Not listed in tests/tap/schedule; run on demand:
  pgaftest run tests/tap/specs/debug_failover_pg19.pgaf
teardown_module() is not a safe place to run DDL that depends on the
cluster being in a clean state.  If the final test perturbs the cluster
(e.g. test_011_start_worker2b_again reattaches a failed node), Citus
metadata sync may still be in progress when teardown fires immediately
afterwards.  The DROP TABLE then fails with:

  ObjectNotInPrerequisiteState: <node> is a metadata node, but is out of sync

Because the exception is raised before cluster.destroy(), all background
pg_autoctl and postgres processes stay alive inside the test container,
keeping the docker run from exiting.  The job then burns its remaining
CI budget until the 15-minute hard timeout fires — consistently across
PG14, 15, 16, and 17.

Fix: move wait_until_metadata_sync() + DROP TABLE into a numbered test
step that runs while the cluster is in a known-stable state, and reduce
teardown_module() to cluster.destroy() only.

- test_basic_citus_operation: test_015_drop_table (after coordinator failover)
- test_citus_multi_standbys:  test_012_drop_table (after worker2b rejoins as secondary)
- test_citus_force_failover:  test_004b_drop_table (before node-drop tests)
- test_nonha_citus_operation: test_007b_drop_table (before primary failures)
wait_until_metadata_sync() reflects the coordinator's view of sync
completion, but a recently-rejoined worker (e.g. after re-attach as
secondary) may still be applying Citus metadata updates when the
coordinator considers sync done.  DROP TABLE then fails with:

  ObjectNotInPrerequisiteState: <node> is a metadata node, but is out of sync

Two related fixes:

1. Add DataNode.citus_run_ddl_after_sync(query, timeout=60): a helper
   that calls wait_until_metadata_sync() then the DDL, and retries the
   whole pair with 1s back-off if ObjectNotInPrerequisiteState is raised,
   for up to timeout seconds.  Use it everywhere DROP TABLE t1 appears
   after a metadata-sync wait.

2. Remove test_004b_drop_table from test_citus_force_failover: this test
   intentionally leaves workers in disrupted states (worker1a dead, worker1b
   in wait_primary with no secondary).  There is no clean stable point to
   run Citus DDL here; DROP TABLE coverage is provided by the other citus
   test files.

Verified: 77 tests pass cleanly across two consecutive local runs on PG16.
…octl node init

Summary table (pgaftest)
  After compose down completes, pgaftest prints a pg_regress-style per-step
  table to stderr showing timing and result for every step:

      test test_003_create_t1              ... ok           (   432 ms)
      test test_022_detect_network_partition ... FAILED     ( 31204 ms)

  Timing is measured with clock_gettime(CLOCK_MONOTONIC) around each
  runner_exec_step() call.  The table appears after teardown{} (which
  includes compose down) so it does not interleave with live logging.
  On fail-fast runs only the completed steps appear.

pg_autoctl drop node --destroy / --force coverage
  New spec tests/tap/specs/drop_node_destroy.pgaf exercises:
    - pg_autoctl drop node --no-wait  (PGDATA kept)
    - pg_autoctl drop node --destroy  (PGDATA removed)
    - pg_autoctl drop node --destroy --force  (no monitor; PGDATA removed)
  Added to multi-misc.sch schedule.

pg_autoctl node init
  New subcommand in cli_node.c that runs pg_autoctl create <kind> without
  --run, initialising a node's PGDATA without starting the supervisor.
  Useful in Dockerfile build stages to pre-bake initdb into image layers.
  Idempotent: exits 0 if PGDATA is already initialised.

CI image caching (run-pgaftest.yml)
  Switch docker build to docker buildx build with --cache-from/--cache-to
  type=gha per version so that the apt-heavy base/citus layers are reused
  across CI runs when the Dockerfile hasn't changed (saves 3-6 min per
  PG version per build job).

Prebuilt monitor image (Dockerfile + compose_gen.c)
  New Dockerfile target 'prebuilt' extends 'run' by running pg_autoctl
  node init for the monitor, baking initdb into a named layer.  The
  compose generator picks PGAF_PREBUILT_IMAGE for the monitor service so
  pg_autoctl node run skips initdb and starts immediately.  Uploaded as
  a separate artifact and passed as PGAF_PREBUILT_IMAGE in test jobs.
- Dockerfile.base: add apt packages for PG18 and PG19; add Citus builds
  with CITUSTAG_18=v14.1.0 and CITUSTAG_19=none (no Citus support yet)
- Makefile.docker: extend PGVERSIONS to 14-19, update default to PG19
- ci.yml: add PG18 and PG19 to build_run_images, build_test_images, and
  test_python matrices; add quick/node/ssl pgaftest jobs for PG18 and PG19;
  add citus pgaftest jobs for PG18/19 with continue-on-error (PG18 Citus
  needs validation, PG19 Citus not yet released); exclude citus pytest jobs
  for PG19; mark PG18 citus pytest as continue-on-error
During the rebase onto main, two versions of cli_node_init and
cli_node_init_getopts were merged into cli_node.c.  The first (simpler)
version used node_do_init() and was a stale intermediate; the second,
complete version uses nodespec_create_argv() + execv() to delegate to
pg_autoctl create <kind>.  Remove the duplicate first definition.
@dimitri dimitri force-pushed the fix/pg17-18-19-support branch from ff95fbe to 2c991af Compare July 15, 2026 01:29
dimitri added 4 commits July 15, 2026 03:41
PostgreSQL 19 adds -Wold-style-definition to its CFLAGS.
exit_internal_error() with an empty parameter list is an old-style C
definition; use (void) to silence the warning.
Previously ensure_base called base-image.yml with force_rebuild=false
(the default), so the workflow reused the existing registry image even
when Dockerfile.base was modified in the PR.  This meant PG18/PG19 run
and test images silently built against the old base, failing at runtime.

Add a check_base_changed job that diffs Dockerfile.base against the PR
base SHA and passes force_rebuild=true to ensure_base when it changed.
CITUSTAG_19=none means no Citus shared library is built into the base
image for PG19.  Running the citus-1/citus-2 schedules against PG19
will always fail at the Citus load step.  Remove the jobs rather than
marking them continue-on-error — they provide no signal until Citus
cuts a PG19-compatible release.
The test called worker2a.fail() in test_004_001 then, in the next test
function (test_004_002), expected an INSERT to raise because worker2a
was dead.  On a fast CI runner the monitor's health-check fired and
worker2b promoted to wait_primary in the ~8 s of pytest inter-test
overhead, so the INSERT succeeded and @raises(Exception) failed.

Fix: inline the write check directly inside test_004_001 immediately
after fail(), before any pytest overhead.  If the failover completes
before we even reach the INSERT the assertion is vacuously satisfied
(the failover did its job); if it hasn't, Citus raises and we catch it.
Remove the now-redundant test_004_002 function.
@dimitri dimitri changed the title Add PostgreSQL 17, 18, and 19 support + graceful keeper shutdown Add PostgreSQL 17, 18, and 19 support Jul 15, 2026
@dimitri dimitri merged commit 56bf6d6 into main Jul 15, 2026
72 checks passed
@dimitri dimitri deleted the fix/pg17-18-19-support branch July 15, 2026 13:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PostgreSQL 18 support and tagged release timeline Release v2.3 with PostgreSQL 18 support is planned?

1 participant