Skip to content

Track connected Postgres server version and Citus extension version#1157

Merged
dimitri merged 1 commit into
mainfrom
feature/node-pg-citus-version-tracking
Jul 25, 2026
Merged

Track connected Postgres server version and Citus extension version#1157
dimitri merged 1 commit into
mainfrom
feature/node-pg-citus-version-tracking

Conversation

@dimitri

@dimitri dimitri commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Summary

pgautofailover.node gains four nullable columns: pg_versionnum (int), pg_version (text), pg_versionstring (text), citus_version (text). These capture the connected node's server_version_num/server_version/version(), and its installed Citus extension version when present (NULL otherwise).

Design

  • Separate SQL API, not routed through node_active(). pgautofailover.report_postgres_version() is a standalone, non-STRICT function (fields can be NULL) called once per Postgres restart, not on every periodic report — this data can't change without a restart, so it stays off node_active()'s hot path.
  • Edge detection uses a dedicated tracker, not LocalPostgresServer.pgIsRunning directly. pgIsRunning is written from multiple places (fsm_transition.c, primary_standby.c, keeper_update_pg_state() itself), so comparing it to its own previous value inside keeper_update_pg_state() misses the first, most important transition — Postgres coming up for the first time. PostgresVersionInfo.lastKnownRunning is written only by keeper_update_pg_state(), giving it a reliable not-running → running signal independent of every other writer.

Implementation

  • pgsql_get_postgres_version() (src/bin/common/pgsql.c): one query fetching version info plus Citus's extversion via a scalar subquery.
  • monitor_report_postgres_version() (src/bin/pg_autoctl/monitor.c): sends it to the monitor.
  • ReportAutoFailoverNodeVersion() (src/monitor/node_metadata.c): plain SPI UPDATE by nodeid, no lock or existence check — a keeper self-report, same posture as ReportAutoFailoverNodeState() (silent no-op on an unknown node, unlike operator-facing commands such as set_node_region).
  • pgautofailover.report_postgres_version() (src/monitor/pgautofailover.sql / node_active_protocol.c): the SQL-callable entry point.

Testing

  • Extended node_active_protocol.sql/.out with direct SQL coverage of report_postgres_version() (NULL handling, unknown/null node_id).
  • New postgres_version_tracking.pgaf pgaftest spec, added to the quick schedule, exercises the keeper-side edge detection end to end against a live node.
  • make docker-check and ci/banned.h.sh: clean. Full monitor regression (12 regress + 6 isolation) passes. config_get_set.pgaf and the full basic_operation.pgaf pass, confirming the keeper_update_pg_state() change doesn't affect the core FSM lifecycle.

pgautofailover.node had no version column at all -- only the keeper's
local state file tracked control-data version info (pg_control_version/
catalog_version_no), never the running server's own server_version_num/
server_version/version(), and Citus's installed version wasn't tracked
anywhere in this codebase. Adds four nullable columns: pg_versionnum
(int), pg_version (text), pg_versionstring (text), citus_version (text,
NULL whenever Citus isn't installed).

Design choices, each a deliberate deviation from the initial plan after
digging into the actual code:

- Three plain scalar columns, not a composite type. No precedent exists
  anywhere in this codebase for a composite CREATE TYPE used as a table
  column or SPI parameter (the only CREATE TYPE is the replication_state
  enum) -- would have been a first, for no real benefit over matching
  the existing all-scalar convention (region, sysidentifier, ...).

- A dedicated, separate SQL API (pgautofailover.report_postgres_version),
  not threaded through node_active(). Postgres/Citus version can't change
  without a Postgres restart, so there's no reason to carry it on every
  few-second periodic report; it's fetched and reported once per restart
  instead, keeping node_active()'s hot path untouched.

- Fired at both keeper start and any later detected Postgres restart, not
  just keeper start. This surfaced a real bug worth documenting: a naive
  "compare pgIsRunning to its previous value" edge check is unreliable,
  because LocalPostgresServer.pgIsRunning is also written directly by
  fsm_transition.c/primary_standby.c during FSM transitions (e.g. the
  very first init -> single transition), outside of
  keeper_update_pg_state() entirely. By the time keeper_update_pg_state()
  next ran, pgIsRunning could already read true from one of those other
  writers, making the most important edge -- Postgres coming up for the
  first time -- invisible. Fixed with a dedicated tracker
  (PostgresVersionInfo.lastKnownRunning) that only
  keeper_update_pg_state() itself ever writes, decoupled from every other
  writer of pgIsRunning.

Monitor side: pgsql_get_postgres_version() (src/bin/common/pgsql.c) runs
one query (server_version_num/server_version/version(), plus Citus's
extversion via a scalar subquery -- NULL when Citus isn't installed) on
the not-running -> running edge; monitor_report_postgres_version()
(monitor.c) sends it via the new SQL function, deliberately not STRICT
so the four fields can be NULL; ReportAutoFailoverNodeVersion()
(node_metadata.c) does the plain SPI UPDATE, no lock or existence check
needed -- a keeper self-report on its own already-known nodeid, same
posture as ReportAutoFailoverNodeState()'s own report path (silent no-op
on an unknown node, unlike operator-facing commands like
set_node_region).

Tests: extended the existing node_active_protocol.sql/.out with direct
SQL coverage of report_postgres_version() (NULL handling, unknown/null
node_id); new postgres_version_tracking.pgaf pgaftest spec exercises the
real keeper-side edge detection end to end against a live node, added to
the quick schedule. Verified config_get_set.pgaf and the full 27-step
basic_operation.pgaf are unaffected by the keeper_update_pg_state()
change.
@dimitri dimitri self-assigned this Jul 25, 2026
@dimitri dimitri added the enhancement New feature or request label Jul 25, 2026
@dimitri
dimitri merged commit e156a5f into main Jul 25, 2026
213 of 216 checks passed
@dimitri
dimitri deleted the feature/node-pg-citus-version-tracking branch July 25, 2026 01:27
dimitri added a commit that referenced this pull request Jul 26, 2026
CI's upgrade.pgaf job failed: an in-place ALTER EXTENSION UPDATE TO '2.3'
left several functions this branch (and two other already-merged PRs)
added to the fresh-install script completely absent from the incremental
2.2--2.3 upgrade script, so any node calling them post-upgrade got
'function does not exist'.

Ports over, matching the fresh-install definitions in pgautofailover.sql
exactly:
- set_node_region() (#1156)
- the four pg_version*/citus_version columns and report_postgres_version()
  (#1157)
- node_timeline_history / accepted_timeline tables and
  report_timeline_history() / accept_timeline() /
  resolve_accepted_timeline() / node_timeline_status() (this branch)

Verified with make -C tests/upgrade all (builds pgaf:current from the v2.2
tag and pgaf:next from this branch, then runs tests/tap/specs/upgrade.pgaf
end to end against a live 3-node cluster) -- all 10 steps pass, including
the convergence step that previously failed.
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.

1 participant