Skip to content

pgaftest: shared common/ library, .pgaf test suite, CI matrix, docs#1132

Open
dimitri wants to merge 52 commits into
mainfrom
pgaftest-infra-combined
Open

pgaftest: shared common/ library, .pgaf test suite, CI matrix, docs#1132
dimitri wants to merge 52 commits into
mainfrom
pgaftest-infra-combined

Conversation

@dimitri

@dimitri dimitri commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR lands the full pgaftest infrastructure on top of the `pg-autoctl-node` work:

  • `src/bin/common/` — shared static library (`libpgaf_common.a`) containing ~20 utility files moved from `src/bin/pg_autoctl/`; both `pg_autoctl` and `pgaftest` link against it
  • `pgaftest` binary — reads `.pgaf` spec files, runs headless CI (TAP output) or interactive setup with Docker Compose
  • 35 spec files in `tests/tap/specs/` porting the Python test suite
  • CI workflow `.github/workflows/run-pgaftest.yml` — full PG 14–17 matrix (4 × 19 specs = 76 jobs)
  • Docs — `docs/ref/pgaftest.rst` reference manual, Testing section in operations, pgaftest alternatives in both tutorials

Key behaviours

`pgaftest run`

Headless CI mode: compose up → setup{} → sequence steps (TAP output) → teardown{} → exit code.

`pgaftest setup --tmux`

Interactive mode: compose up → monitor ready → tmux opens immediately — logs pane, watch pane, setup running in bottom pane. When setup completes, bottom pane becomes an interactive bash shell in the first data node.

`pgaftest down --work-dir `

Fixed: now derives project name from the workDir basename and runs `docker compose -p -f

/docker-compose.yml down` instead of a bare no-op.

Fixes in this PR

  • `setup --tmux` no longer blocks at a blank terminal for the full setup timeout before opening tmux
  • `pgaftest down --work-dir` now actually tears down the correct stack
  • Dockerfile: `apt-key` deprecation warnings replaced with `gpg --dearmor` + `signed-by=`
  • C: two `-Wmissing-prototypes` warnings fixed (`pgsql_alter_role_password`, `keeper_cli_pgsetup_hba_lan`)

Interactive tutorial spec files

  • `docs/tutorial/interactive_tutorial.pgaf` — two-node HA cluster for hands-on exploration
  • `docs/tutorial/citus_tutorial.pgaf` — eight-node Citus cluster (coordinator + 3 worker pairs)

dimitri added 21 commits July 6, 2026 23:35
…de.ini

Adds a new pg_autoctl node sub-command tree and a NodeSpec file format
designed as the recommended entry-point for container and Kubernetes
deployments.

## pg_autoctl_node.ini sections

  [node]         kind (postgres|monitor|coordinator|worker), name, hostname, port
  [postgresql]   pgdata
  [monitor]      pguri (empty for monitor nodes), no_monitor, node_id
  [formation]    name, group
  [settings]     candidate_priority, replication_quorum   ← mutable, applied live
  [options]      ssl, auth, pg_hba_lan                    ← create-time only
  [ssl]          ssl_ca_file, ssl_cert_file, ssl_key_file
  [launch]       mode=deferred: wait for pg_autoctl node start
  [formation N]  monitor only: additional named formations

## pg_autoctl node sub-commands

  run   <file>   Read ini, create node if absent, exec() into supervisor.
                 Sets PG_AUTOCTL_NODESPEC so the supervisor watches for
                 live [settings] changes via inotify (Linux) or mtime poll.
  apply <file>   Converge mutable settings on an already-running node.
  start [<file>] Clear launch=deferred so a waiting node run proceeds.
  show           Dump live config as pg_autoctl_node.ini on stdout.
  check <file>   Parse-only validate; print resolved fields.

## Supervisor file watcher (nodespec_watcher)

The supervisor initialises a NodeSpecWatcher when PG_AUTOCTL_NODESPEC is
set. Every tick it checks for file changes:
  - Linux:   drain inotify IN_CLOSE_WRITE / IN_MOVED_TO events
  - Others:  stat() every NODESPEC_WATCH_INTERVAL_SECS (10 s)
On change, re-parse [settings] and call nodespec_apply() to converge
mutable fields without restarting the node.

## Files

  src/bin/pg_autoctl/cli_node.c / cli_node.h
  src/bin/pg_autoctl/nodespec.c / nodespec.h
  src/bin/pg_autoctl/supervisor.c / supervisor.h  (watcher integration)
  docs/ref/pg_autoctl_node.rst
  docs/ref/pg_autoctl_node_run.rst
- pg_autoctl_node.rst: replace prose property list with a structured
  table (section/property/type/mutable/default/description), each
  property on its own row; fix 'disabled-monitor' label → actual ref

- nodespec_apply: add ssl apply block — when [options].ssl or any [ssl]
  cert path changes, call 'pg_autoctl enable ssl' with the appropriate
  flags (--ssl-self-signed, --no-ssl, or --ssl-mode + cert paths);
  update nodespec_apply comment to list ssl as mutable

- docs/index.rst: remove 'Container and Kubernetes' as a top-level
  toctree caption; the manual page is still reachable via Manual Pages

- docs/operations.rst: add 'Container and Kubernetes Deployments'
  section describing pg_autoctl node run, live reconfiguration, and
  the launch=deferred pattern; cross-refs to pg_autoctl_node for details

- docs/ref/configuration.rst: add 'Declarative Node Configuration'
  section covering pg_autoctl_node.ini sections and their relationship
  to pg_autoctl.cfg; cross-ref to pg_autoctl_node
When the [monitor] pguri changes in pg_autoctl_node.ini, nodespec_apply
now re-registers the node to the new monitor without stopping Postgres:

  pg_autoctl disable monitor --force --pgdata <dir>
  pg_autoctl enable monitor <new_uri> --pgdata <dir>

The disable step removes the node from the old monitor (--force allows
this even if the old monitor is temporarily unreachable). The enable step
registers the node to the new monitor and signals the running supervisor
to start using the new monitor_pguri for node_active calls.

Document the change in the property table and Live Reconfiguration section
of pg_autoctl_node.rst.
…ction

Follow the pg_autoctl_create pattern: each pg_autoctl node sub-command
now has its own manual page, all listed in pg_autoctl_node.rst's toctree:

  pg_autoctl node run    (existing)
  pg_autoctl node apply  (new)
  pg_autoctl node start  (new)
  pg_autoctl node show   (new)
  pg_autoctl node check  (new)

Remove the redundant 'Container and Kubernetes Deployments' section from
operations.rst. The full documentation for this feature lives in the
pg_autoctl_node manual page (ref/pg_autoctl_node.rst) and its sub-pages.
The list-table rendered poorly in the Sphinx HTML theme. Replace it with
the same definition-list style used throughout the other manual pages
(e.g. pg_autoctl_create_postgres Options section): each property is a
bare term followed by indented description paragraphs, grouped under
ini-section sub-headings.
…ioning

Re-add the 'Container and Kubernetes Deployments' section at the end of
operations.rst with a named anchor so it can be referenced from elsewhere.

Add a short note in the Provisioning section pointing to pg_autoctl node run
as the declarative alternative for container and Kubernetes deployments,
with a cross-reference down to the new section.
Add references to pg_autoctl node / pg_autoctl node run in all the places
identified by the documentation audit:

Reference pages (new See Also section):
- pg_autoctl_create_postgres.rst
- pg_autoctl_create_monitor.rst
- pg_autoctl_create_coordinator.rst
- pg_autoctl_create_worker.rst
- pg_autoctl_run.rst

Narrative docs (short note pointing to the declarative alternative):
- how-to.rst — after the pg_autoctl run step in Quick Start
- tutorial.rst — after the docker-compose.yml literalinclude
- citus-quickstart.rst — after the docker-compose-scale.yml literalinclude
- install.rst — after the systemd unit section
Replace the imperative pg_autoctl create postgres / pg_autoctl create
monitor commands with the declarative pg_autoctl node run approach:

- Add tutorial/ini/monitor.ini and tutorial/ini/postgres.ini — two small
  ini files that describe the monitor and every data node respectively.
  Data nodes share one ini file; hostname and name default to the
  container hostname set by Docker Compose.

- Rewrite tutorial/docker-compose.yml: all PG_AUTOCTL_* environment
  variables are gone; each service bind-mounts its ini file at
  /etc/pgaf/node.ini and runs 'pg_autoctl node run'. The x-node anchor
  is now clean — no env vars, single command.

- Update tutorial.rst to introduce the ini files before the compose file,
  explain that pg_autoctl node run handles both create and run, and show
  that live reconfiguration (candidate_priority change for node3) is done
  by editing the ini file rather than calling pg_autoctl set.

- Replace the stale pg_autoctl_do_tmux_compose_session reference in Next
  steps with a pointer to the pg_autoctl_node reference and the Container
  and Kubernetes Deployments section.
… paths

Show both approaches side by side:

- Direct command (pg_autoctl set candidate-priority): immediate, no restart
- Declarative ini file: explain that changing docker-compose.yml volumes
  requires 'docker compose up -d node3' to recreate the container, that
  pg_autoctl node run applies the ini diff on startup before exec'ing into
  the supervisor, and that once the dedicated ini file is mounted any
  subsequent edits to it are picked up live by the running supervisor.
Replace all pg_autoctl create coordinator/worker/monitor commands with
the declarative pg_autoctl node run approach:

New ini files:
  citus/ini/monitor.ini     — kind = monitor
  citus/ini/coordinator.ini — kind = coordinator, shared by coord0a/coord0b
  citus/ini/worker.ini      — kind = worker, no group (monitor auto-assigns)
  citus/ini/worker1.ini     — kind = worker, group = 1
  citus/ini/worker2.ini     — kind = worker, group = 2
  citus/ini/worker3.ini     — kind = worker, group = 3

Rewritten compose files:
  docker-compose-scale.yml  — three services (monitor/coord/worker), each
    bind-mounts its ini file; all PG_AUTOCTL_* env vars removed
  docker-compose.yml        — named services with per-group worker ini files;
    YAML anchors kept for coord and per-group worker templates

Updated citus-quickstart.rst:
  - Show all ini files with literalinclude captions before each compose file
  - Explain that worker.ini without [formation] group triggers monitor
    auto-assignment (right for --scale), while workerN.ini with group = N
    pins the pair to a specific shard group (right for named services)
  - Explain that pg_autoctl node run uses the container hostname as the
    node name when name is not set in the ini file
  - Replace stale pg_autoctl_do_tmux_compose_session reference in Next
    steps with pointer to pg_autoctl_node and container deployments guide
…pec suite

Combines what would have been two separate PRs:
- src/bin/common/: move ~20 utility sources (pgsql, pgsetup, ini_file, …) out
  of pg_autoctl/ into a shared static library linked by both binaries
- src/bin/pgaftest/: new test binary — reads .pgaf specs, runs headless CI
  (TAP output) or interactive setup with docker compose
- tests/tap/specs/: 35 .pgaf spec files covering the full test matrix
- tests/tap/schedule: ordered run list for CI
- tests/upgrade/: dual-binary upgrade test image and shim script
- .github/workflows/run-pgaftest.yml: CI workflow — builds pgaf:run image and
  pgaftest binary once, fans out one job per spec in parallel
- Dockerfile: add pgaftest build/install target, bison touch guard
- style: all new C files reformatted with citus/stylechecker:no-py Docker image
Split the single build job into:
- build-images: matrix over PG14/15/16/17, one pgaf:run-pgNN artifact each
- build-pgaftest: single job (PG17), binary is version-agnostic at runtime

The test matrix now crosses every spec × every PG version (19 specs × 4
versions = 76 parallel jobs), matching the run-tests.yml coverage scope.
installcheck likewise runs on all four versions.

This positions pgaftest as the replacement for run-tests.yml once all
scenarios are ported — run-tests.yml will be removed at that point.
Three panes, laid out evenly:
  top    — docker compose logs -f  (all services)
  middle — pg_autoctl watch        (on the monitor)
  bottom — bash inside the first non-deferred data node

Previously the session was created detached (-d) and the user had to
manually run 'tmux attach'. Now runner_setup attaches immediately after
creating the session so the terminal lands inside tmux.

The shell pane target is the first non-deferred data node in the spec's
formation block; falls back to the monitor when no data node is found.
…c --tmux)

The commandline library calls getopt, which on POSIX/macOS stops scanning
at the first non-option argument (the spec file path).  Any flags given
after the spec file — the natural invocation form for setup/run -- are
silently ignored, so --tmux and --work-dir have no effect.

Fix: after cli_setup extracts argv[0] as the spec path, reset getopt state
(optreset=1 on BSD, optind=1 everywhere) and call pgaftest_getopts again
on the remaining argv.  argv[0] serves as a dummy program name so getopt
starts scanning from index 1, picking up --tmux, --work-dir, etc.

Also applies to the tmux session creation:
- session is now attached immediately (tmux attach-session after new-session -d)
- third pane execs bash inside the first non-deferred data node
setup --tmux timing
  The setup{} block now runs inside the tmux bottom pane instead of
  before tmux is created.  The user immediately gets the three-pane
  session; the bottom pane shows setup progress and transitions to an
  interactive bash shell when done.

  - add runner_run_setup_only() — runs setup{} against an existing stack
  - add pgaftest _setup_ sub-command — internal entry-point for the pane
  - runner_setup() launches tmux first (after compose up + monitor ready)
    then the bottom pane calls _setup_ and exec's bash on success
  - set pg_autoctl_program to argv[0] in main() so runner_setup can
    reference the binary path in the tmux pane command

pgaftest down --work-dir
  When no spec file is found, cli_down now derives the project name from
  the workDir basename and runs:
    docker compose -p <name> -f <workDir>/docker-compose.yml down ...
  instead of a bare docker compose down that silently targets nothing.

docs
  - docs/ref/pgaftest.rst: full reference (sub-commands, DSL, env vars, TAP)
  - docs/operations.rst: new Testing section
  - docs/tutorial.rst: pgaftest alternative section with interactive_tutorial.pgaf
  - docs/citus-quickstart.rst: pgaftest alternative with citus_tutorial.pgaf
  - docs/index.rst: add ref/pgaftest to toctree
  - docs/tutorial/interactive_tutorial.pgaf: two-node HA interactive spec
  - docs/tutorial/citus_tutorial.pgaf: eight-node Citus interactive spec
Dockerfile: replace deprecated apt-key with gpg --dearmor
  Three stages (base/build, run, testrun) were using:
    curl ... | apt-key add -
  which triggers a deprecation warning in recent apt versions.
  Replaced with the modern approach:
    gpg --dearmor -o /usr/share/keyrings/pgdg-archive-keyring.gpg
  and added signed-by= to the sources list entry.

C: add missing prototype declarations
  - pgsql_alter_role_password() declared in common/pgsql.h was not
    visible when pg_autoctl/pgsql.h was included first; added to
    pg_autoctl/pgsql.h so the declaration is found regardless of
    include path ordering.
  - keeper_cli_pgsetup_hba_lan() in cli_do_misc.c was missing from
    cli_do_root.h; added alongside the other pgsetup helpers.
@dimitri dimitri self-assigned this Jul 6, 2026
@dimitri dimitri added enhancement New feature or request Developer productivity Enhancements to ability to ship quality code Packaging and CI Enhancements to our CI integration labels Jul 6, 2026
dimitri added 5 commits July 7, 2026 01:29
The 16 .c/.h files that were moved to src/bin/common/ were still present
in src/bin/pg_autoctl/ as copies.  pg_autoctl was compiling them locally
via the SRC = $(wildcard *.c) glob AND linking libpgaf_common.a, causing
duplicate symbol resolution (local .o silently winning over the archive).

All 32 files (16 .c + 16 .h pairs) are removed; pg_autoctl now uses only
the common/ versions via libpgaf_common.a and its include path.

The 6 files that had minor divergences were analysed:
- ini_file.c / ipaddr.c / pgsetup.c / signals.c: only brace-style
  differences from citus_indent; common/ version is correct.
- pgctl.c: common/ is a superset (adds --checkpoint=fast for
  pg_basebackup, fixes intToString dangling-pointer use).
- pgsql.c: common/ is a superset (adds pgsql_alter_role_password).
Both .c and .h pairs for pgctl and pgsql were checked; common/ contains
all declarations.
Replace strncpy/strncat with strlcpy/strlcat in hand-written pgaftest
source files (test_runner.c, cli_root.c, cli_indent.c, test_spec_parse.c),
and add IGNORE-BANNED to unavoidable memcpy calls.

Exclude src/bin/pgaftest/ from citus-style gitattribute so that the
banned.h.sh check does not flag pgaftest's legitimate use of standard
C I/O (fopen, fprintf, snprintf, getenv, atoi) — which is appropriate
for developer test infrastructure but banned in production code.

The Flex/Bison generated files (test_spec_scan.c, test_spec_parse.c)
are covered by the same exclusion.
v13.0.1 dropped PostgreSQL 14 support (configure: error: Citus is not
compatible with the detected PostgreSQL version 14).  v12.1.5 is what
main uses and supports PG14 through PG17.
Replace banned C stdlib calls with the project's safe wrappers:
- snprintf() → sformat()     (wraps pg_vsnprintf; from file_utils.h)
- fprintf()  → fformat()     (wraps pg_vfprintf; from file_utils.h)
- vsnprintf() → pg_vsnprintf()
- printf()   → fformat(stdout, ...)
- atoi()     → stringToInt() (from string_utils.h)
- strncat()  → strlcat()     (already done in earlier commit)
- strncpy()  → strlcpy()     (already done in earlier commit)

For calls with no safe-wrapper alternative (fopen, getenv, sscanf, memcpy)
that already follow the documented safe-use pattern, add /* IGNORE-BANNED */
on the calling line — matching the approach used in pg_autoctl/nodespec.c
and common/env_utils.c.

The Bison/Flex generated files (test_spec_parse.c, test_spec_scan.c) use
banned functions inside Bison/Flex boilerplate; those lines get
/* IGNORE-BANNED */ since we do not regenerate the grammar mid-build.

Add #include "file_utils.h" and #include "string_utils.h" to the files
that now call sformat/fformat/stringToInt.

Remove the src/bin/pgaftest/** -citus-style .gitattributes line added in the
previous commit; pgaftest is now clean under both citus_indent and
ci/banned.h.sh.
dimitri added 26 commits July 7, 2026 14:07
Citus v12.x supports PG14–15; v13.x supports PG15–17.  No single tag
covers the full matrix.  The top-level Makefile already handles this:

  BUILD_ARGS_pg14 = --build-arg CITUSTAG=v12.1.5
  BUILD_ARGS_pg15 = --build-arg CITUSTAG=v12.1.5
  BUILD_ARGS_pg16+ = --build-arg CITUSTAG=$(CITUSTAG)   # v13.2.0

run-pgaftest.yml was calling docker build with no CITUSTAG override, so
it always used the Dockerfile ARG default and broke on either end:
  - v13.0.1 → configure: Citus not compatible with PG14
  - v12.1.5 → configure: Citus not compatible with PG17

Fix: inline the same version map in the workflow using a shell case
statement.  Also bump the Dockerfile ARG default to v13.2.0 to match
the Makefile.
Align run-pgaftest.yml with the versions already used in run-tests.yml:
  actions/checkout          v4.2.2 → v7.0.0
  actions/upload-artifact   v4.4.3 → v7.0.1
  actions/download-artifact v4.1.8 → v8.0.1
Five root causes fixed:

1. pg_autoctl inspect pgsetup hba-lan — unknown command
   Add hba-lan to do_pgsetup[] subcommand array and define
   do_pgsetup_hba_lan CommandLine entry in cli_do_root.c.

2. pg_autoctl inspect pgsetup wait --timeout N — unrecognized option
   Add a dedicated keeper_cli_pgsetup_wait_getopts() that accepts
   --pgdata and --timeout.  Default timeout remains 30 s; any positive
   integer can be supplied via --timeout.

3. pg_autoctl drop node --no-wait — unrecognized option
   Add --no-wait to cli_drop_node_getopts().  Sets
   listen_notifications_timeout to 0, which the existing code already
   treats as 'skip waiting'.

4. pg_autoctl create postgres/monitor reject --replication-password,
   --monitor-password, --autoctl-node-password generated by pg_autoctl
   node run from a nodespec.ini with passwords configured, causing
   container auth-node1-1 to exit(1):
   - Add --replication-password / --monitor-password to
     cli_create_postgres_getopts via cli_common_keeper_getopts ('e'/'w').
     replication_password is stored in KeeperConfig (already used when
     creating pgautofailover_replicator); monitor-password is accepted
     and ignored (health-check role uses a hardcoded password for now).
   - Add --autoctl-node-password to cli_create_monitor_getopts ('e').
     Stored in the new MonitorConfig.autoctl_node_password field and
     written to the ini file.  monitor_install() accepts the password
     as a new parameter and calls pgsql_alter_role_password() on
     autoctl_node after extension creation when non-empty.

5. installcheck PG14 Citus incompatibility
   The installcheck job was building --target testrun without passing
   CITUSTAG, so the Dockerfile default v13.2.0 was used — incompatible
   with PG14.  Add the same case/esac per-version CITUSTAG logic that
   build-images already uses.
Increase several wait-until timeouts from 120s to 180s in specs that
were hitting the limit on GitHub Actions shared runners:
- ssl_cert: setup waits longer for SSL cert auth cluster to form
- ensure: test_004_demoted failover wait
- multi_async: test_011 report_lsn wait for node4
- multi_alternate: test_005_003 demoted/secondary wait for node2
- multi_ifdown: test_008_failover wait_primary/primary wait for node3

Also fix config_get_set: replace hardcoded /usr/lib/postgresql/17/bin/pg_ctl
with /bin/pg_ctl since expect{} does substring matching and the path
contains the actual PGVERSION, not always 17.
Replace reinvented C implementations with the already-tested versions
from the original pgaftest-infra branch (origin/pgaftest-infra):

- cli_do_misc.c: restore proper keeper_cli_pgsetup_wait_getopts that
  delegates to keeper_cli_keeper_setup_getopts after stripping --timeout
  and --read-write (handles PGDATA env var correctly); restore full
  wait_until_ready with config-file wait loop and --read-write phase
- cli_do_root.c/h: restore correct --read-write/--timeout help text and
  hba-lan command description
- cli_common.c: restore monitor_password (case 'W') and
  replication_password (case 'w') in cli_common_keeper_getopts
- cli_create_node.c: restore correct option letter assignments and
  --formation support in monitor create getopts
- cli_drop_node.c: restore --no-wait implementation
- keeper_config.h/c: restore monitor_password field and ini option
- monitor_config.h/c: restore autoctl_node_password field and ini option
- monitor_pg_init.h/c: restore autoctl_node_password parameter
- service_monitor_init.c: restore autoctl_node_password call
- nodespec.c/h: restore full monitor_uri change handling and password
  argv building

Also update workflow to use schedule-based matrix (76→24 jobs) and
add tests/tap/schedules/ directory.
- keeper_cli_help() referenced root_with_debug which was removed as part
  of the inspect/manual refactor (root_with_debug was only needed when do
  commands were hidden behind PG_AUTOCTL_DEBUG).  Replace with a direct
  call using root, which already includes inspect/manual/internal.

- cli_root.c root_subcommands referenced &internal_commands and &do_commands
  as two separate entries, but cli_do_root.c only defined do_commands.
  Rename do_commands → internal_commands in cli_do_root.c/h and drop the
  duplicate &do_commands slot from root_subcommands.
…in CI

- node-extra.sch: debian_clusters + tablespaces
- citus-1.sch: citus_cluster_name, citus_force_failover, citus_skip_pg_hba
- citus-2.sch: basic_citus_operation, nonha_citus_operation, citus_multi_standbys
- run-pgaftest.yml: expand matrix from 6 to 9 schedules (36 jobs total)

All 27 specs now covered: 9 schedules + installcheck + upgrade dedicated jobs.
basic_operation.pgaf:
- test_015_fail_secondary: wait until node1 stopped 30s → 60s
  (compose stop can be slow on shared CI runners)
- test_016_drop_secondary: same wait until node1 stopped 30s → 60s
- test_022_detect_network_partition: both waits 90s → 150s
  (demote_timeout transition involves monitor health-check cycles;
  shared CI runners can be slow to process the network partition signal)

auth.pgaf:
- setup: wait until primary, secondary 120s → 180s
  (cert-auth cluster formation is slower on shared CI runners)

enable_ssl.pgaf:
- test_007_enable_ssl_secondary: remove pgsetup wait after compose
  stop/start.  node2 is still in maintenance mode when the container
  restarts — the monitor-assigned state is 'maintenance' so pg_autoctl
  does not start Postgres.  The original Python test also does not
  assert Postgres is running at this point (the assert is in test_008).
- test_008_disable_maintenance: add pgsetup wait --timeout 90 after
  disabling maintenance, so we confirm Postgres is up with SSL before
  the replication checks in test_009.
Four root causes diagnosed from the CI run logs; all fixed here.

1. upgrade/test_004_wait_convergence: 3-node post-upgrade reconvergence
   timed out at 180s on loaded shared CI runners.  Bump to 300s.

2. node-extra/debian_clusters (structural):
   a. compose_gen.c: write_image_stanza_target() always used PGAF_IMAGE
      even when target='debian', so the pre-built run image was used
      instead of the Debian-cluster image.  Now checks PGAF_DEBIAN_IMAGE
      for the 'debian' target; falls back to an inline build stanza.
   b. run-pgaftest.yml: build-images now also builds the 'debian' Docker
      target (one extra layer on top of 'run', fast via layer cache) and
      uploads it as a separate artifact.  The test job downloads and loads
      it for the node-extra schedule, and passes PGVERSION and
      PGAF_DEBIAN_IMAGE to the runner.
   c. debian_clusters.pgaf test_002: hard-coded PG17 path replaced with
      $PGDATA (expanded by the shell inside the container at runtime).

3. citus-1/citus_skip_pg_hba: Docker healthcheck window (75s) too short
   for Citus coordinator init on a loaded runner (third Citus spec in
   the schedule).  Increase retries 30→60 (new window: 15+60×2=135s).

4. multi-2/PG15 (two transient-state races):
   - multi_alternate test_005_003: 'demoted' is an intermediate state
     lasting under a second; the pgaftest assigned-state poller misses it
     on loaded runners.  Drop the wait; go straight to the stable end
     state (secondary+primary), bump timeout to 300s.
   - multi_ifdown test_008: 'wait_primary' is likewise transient when the
     standby joins almost immediately.  Merge both waits into a single
     stable-state wait at 300s.
…w to PG17

Spec timeout and transient-state fixes (six of seven diagnosed failures;
report_lsn handled separately — see note below):

* basic_operation.pgaf test_022: demote_timeout wait 150s → 300s.  The
  demote_timeout state requires the monitor to exhaust its health-check
  retry budget against a silently-partitioned node; shared CI runners need
  more than 150 s for that.

* ssl_cert.pgaf setup: 180s → 300s.  verify-ca + cert-auth cluster
  formation is the slowest startup path (TLS handshake validation, ident
  map writing, CA-cert verification for every pg_autoctl connection) and
  is the third spec in the ssl schedule, running on a warm runner.

* maintenance_and_drop.pgaf setup: 90s → 180s.  Setup times out on loaded
  runners; this spec now runs later in the merged node schedule.

* ensure.pgaf test_004_demoted: remove transient 'demoted' wait.  Same
  fix as multi_alternate (committed in prior round): 'demoted' lasts under
  a second and the LISTEN/NOTIFY path cannot reliably catch it.  Wait for
  the stable end state at 300s instead.

* multi_maintenance.pgaf test_009b: 90s → 180s.  Two nodes rejoining
  simultaneously (pg_rewind + replication restart each) can be slow.

* compose_gen.c healthcheck: start_period 15s → 90s (retries already 60).
  New max window: 90 + 60×2 = 210s for Citus coordinator initialization.

Note on report_lsn: runner_wait_notify_goal has a 'torn-read' SQL
confirmation step that re-queries the monitor after a convergence
notification.  For transient states (report_lsn), the monitor may have
already advanced by the time the SQL query executes, causing the function
to retry forever and time out.  The fix belongs in the runner (relax the
SQL confirmation guard for convergence notifications that already carry
both goal and reported state) not in the spec; tracked separately.

Schedule consolidation (option 2):
  Merge monitor and node-extra into node.  These schedules each took under
  2 minutes and wasted 8 GitHub Actions runner slots per run.  The merged
  node schedule now runs all of: create_standby_with_pgdata,
  maintenance_and_drop, auth, monitor_disabled, replace_monitor,
  extension_update, debian_clusters, tablespaces.

PG version reduction (option 3):
  Slow schedules (multi-1, multi-2, citus-1, citus-2) now run PG17 only.
  These test pg_auto_failover FSM logic, not PG version-specific code
  paths.  Result: 3×4 + 4×1 = 16 test jobs instead of 36, eliminating
  the 10-15 min queue wait that was doubling wall-clock time vs the old
  Python suite (which had only 4 jobs and got runners immediately).
…rmation

Transient FSM states (report_lsn, demoted, wait_primary) last less than a
second.  The old code issued a SQL confirmation after the LISTEN notification
fired: by the time that round-trip returned, the monitor had already advanced
past the transient target, so assigned_state and reported_state no longer
matched and the wait fell through to a spurious timeout.

Two changes fix this:

1. Drain-at-start: before the fast-path SQL check, drain any notifications
   already buffered in libpq.  A preceding blocking exec command may have
   accumulated the entire FSM cycle while pgaftest waited for the subprocess.
   Draining on entry means a transient state that arrived and departed during
   the exec is not missed.  It also prevents stale notifications from prior
   waits from satisfying the current one.

2. Remove SQL confirmation: once convergence is detected in the notification
   (goalState == reportedState == targetState), return immediately.  The
   drain-at-start guarantee makes the stale-notification guard unnecessary,
   and the SQL round-trip is what caused the spurious timeout for transient
   states.

monitor_wait_formation_states() already had drain-at-start (lines 1072-1075);
this aligns the single-node path with the same pattern.
wait until node2 state is demote_timeout requires both goalState and
reportedState to equal demote_timeout.  A network-disconnected node can
never report back to the monitor, so reportedState stays at 'primary'
permanently and the wait always times out.

Change to 'wait until node2 assigned-state = demote_timeout', which
only checks the monitor's assignment (goalState).  The monitor does
assign demote_timeout within ~20s of the network disconnect; the
disconnected node's inability to confirm is irrelevant to the test's
purpose of verifying that the monitor detected the partition.

Also increase auth.pgaf setup timeout 180s → 300s: md5 password auth
adds overhead to cluster formation; 180s is too tight on loaded shared
CI runners.
Two independent fixes:

1. ssl client cert for monitor LISTEN connection
   When the cluster uses verify-ca/verify-full SSL with cert auth, the
   monitor's pg_hba.conf requires a client certificate for every
   connection — including the runner's own libpq connection.  Without
   one, runner_notify_connect() fails silently, LISTEN is unavailable,
   and the SQL-polling fallback also fails, so wait-for-state commands
   always timeout regardless of actual cluster state.

   Add sslmode, sslrootcert, sslcert, sslkey parameters to the monitor
   connection string when ssl is verify-ca/verify-full and auth is cert.
   The runner already generates the client cert at <workDir>/ssl/client/
   via compose_gen_write_ssl_certs(); those paths are now used here.

2. Wider Docker healthcheck window for data nodes
   The previous window was 90s start_period + 60 × 2s retries = 210s.
   The third spec in the citus-1 schedule (citus_skip_pg_hba) exhausted
   this window (coord0a became unhealthy at ~212s) on a loaded CI
   runner.  Increase to start_period=120s + 120 × 2s retries = 360s to
   give containers more headroom on shared runners without affecting
   fast-start containers (Docker accepts 'healthy' as soon as the first
   check passes, regardless of start_period).
The client private key at ssl/client/postgresql.key is generated with
mode 0644 so the container user (different UID from the CI runner) can
read it from the read-only bind-mount.  libpq enforces strict key
permissions and refuses to use a key with group/world read access,
causing the LISTEN connection to fail silently.

Copy the key to ssl/client/runner.key at mode 0600 before opening the
libpq connection.  The copy is done once (guarded by access(2)) and
reused across reconnect attempts, so there is no per-retry overhead.
When auth=cert is configured, pg_autoctl writes HBA rules for standby
replication connections with the plain 'cert' method.  Cert auth
requires the client certificate CN to match the database username, but
both monitor and replication connections use the same client certificate
(CN=autoctl_node) while the replication role is pgautofailover_replicator.

Without an ident map the replication connection always fails:
  FATAL: certificate authentication failed for user "pgautofailover_replicator"

Fix keeper_update_group_hba() to:
  1. Use 'cert map=pgautofailover' for the replication HBA rule, matching
     the same pattern already used in cli_do_misc.c for LAN CIDR rules.
  2. Call pghba_ensure_ident_map_entry() to write
       pgautofailover autoctl_node pgautofailover_replicator
     to pg_ident.conf, so the cert CN maps to the replication role.

Verified with ssl_cert.pgaf: all 5 tests pass, formation completes in
~5 seconds.
When --auth md5 is used, upstream_has_replication_slot() needs to supply
the replication password to connect to the upstream node. Previously it
built the connection string without a password, causing authentication
failures when the upstream requires md5. After 5 consecutive failures
wait_until_primary_has_created_our_replication_slot() gives up, the
init process exits, the supervisor restarts it up to 5 times in 300s,
and the container dies -- all while the test waits for the formation to
reach primary/secondary, timing out after 300s.

Fix: set PGPASSWORD from upstream->password before calling pgsql_init(),
following the same save/restore pattern used in pg_basebackup().
Replace the bare-runner pattern (extract binary + apt-get install libpq5)
with a self-contained pgaf:pgaftest Docker image that already bundles
libpq5, docker-ce-cli, and docker-compose-plugin.

build-pgaftest now builds --target pgaftest and uploads the image as an
artifact.  All test/installcheck/upgrade jobs load that image and run
pgaftest via:

  docker run --rm \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -v /tmp/pgaftest:/tmp/pgaftest \
    -v $(pwd):/work:ro \
    pgaf:pgaftest \
    pgaftest run ...

The /tmp/pgaftest bind-mount lets compose stacks started by pgaftest
(inside the container) access the generated INI and compose files via
the same host paths the Docker daemon sees — the standard DooD pattern
on Linux runners.

This eliminates the fragile dependency on Microsoft's apt repositories
that are pre-installed on GitHub's ubuntu-latest image and have been
causing sporadic apt-get update failures (NOSPLIT / auth errors) that
have nothing to do with our code.
pgaf:pgaftest runs as USER docker (UID 1000).  On GitHub's ubuntu-latest
runners /var/run/docker.sock is srw-rw---- root:docker where the docker
GID is ~999, not 1000.  The container's docker group GID doesn't match
the host GID so all docker compose calls fail immediately with permission
denied, causing every test job to exit in <30 seconds.

Fix: run the container as root and add the host docker socket GID via
--group-add so both the socket access and writes to /tmp/pgaftest
(bind-mounted from the runner) work without restriction.
GitHub Actions ubuntu-latest now uses rootless Docker.  Container root
(uid 0) maps to the rootless daemon uid on the host, which is different
from the runner uid that created /tmp/pgaftest (mode 755).  Writes from
inside the pgaf:pgaftest container therefore fail with EACCES when
pgaftest tries to create its per-spec work directories.

Making the bind-mount root world-writable (777) allows any mapped uid
to create sub-directories inside it, restoring write access regardless
of the uid remapping in effect.
assert stays primary while { stop postgres monitor } fails because
the stays-while logic checks the state via the monitor (LISTEN or SQL)
after each body command.  When the body stops the monitor's own postgres,
both the LISTEN connection and the SQL fallback become unavailable, so
the runner reports 'could not get state of node3 after command' and the
assertion fails incorrectly.

The monitor cannot assign state changes while its postgres is down, so
there is nothing to guard against during the outage.  Replace the
stays-while block with a plain stop + sleep + wait, matching the intent
of the original Python test: verify that a brief monitor outage does not
trigger a spurious failover.
…thcheck

multi_ifdown test_014: compose stop node3/node1 caused pg_autoctl perform
failover to block ~68s waiting for dead primaries, leaving only 22s of a 90s
timeout.  Switch to network disconnect (matches the Python test's ifdown()),
run the failover in background so the wait starts immediately, and increase
the timeout to 300s.  Update test_015 to use network connect instead of
compose start.

citus_skip_pg_hba: coord0a started simultaneously with the monitor but
auth=skip leaves the monitor's pg_hba.conf with only localhost rules, so
coord0a's registration attempts from another container all fail and its
healthcheck never passes.  Make coord0a launch deferred; move the monitor
HBA setup into the setup block (before starting coord0a); rename test_000
to reflect its new sole job of adding coord0a's own HBA rules.

compose_gen.c: a deferred node in the first slot (now coord0a) was
incorrectly chosen as the healthcheck anchor.  Its pg_autoctl status always
returns non-zero during sleep infinity, blocking all subsequent nodes via
service_healthy.  Skip deferred nodes when selecting firstNode so the
healthcheck lands on the first node that actually starts postgres.
The pg_rewind after this test requires a clean shutdown of node3 (primary)
and node1 so their WAL is preserved for the rewind-against-node2 step.
network disconnect keeps postgres running; old WAL segments may be recycled
before pg_rewind can use them as the divergence-point anchor.

The real fix is running the failover in background (&) so the 68s blocking
wait for the dead primary does not consume the test_015 timeout budget.
compose stop + background failover gives clean WAL and a non-blocking call.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Developer productivity Enhancements to ability to ship quality code enhancement New feature or request Packaging and CI Enhancements to our CI integration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant