Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b92f46a
Monitor: detect replication stall and assign wait_primary (issue #997)
dimitri Jul 15, 2026
17ac3db
Wire --region through CLI, fix extension versioning, add 3-DC stall test
dimitri Jul 15, 2026
f7c5ab4
Fix installcheck expected outputs for PG18 and PG19
dimitri Jul 15, 2026
f54588f
Fix pgaftest step: copy spec to workdir, derive project from workdir;…
dimitri Jul 15, 2026
c3dad45
Fix watch: update current_state column count guard from 16 to 17
dimitri Jul 15, 2026
9aefb90
src/bin/Makefile: serialise common archive build before parallel bina…
dimitri Jul 15, 2026
307e1f5
pgaftest --tmux: interactive shell inside pgaftest service container
dimitri Jul 15, 2026
072f0dc
Fix --tmux bottom pane: buffer overflow and sh quoting in hint command
dimitri Jul 15, 2026
73d3106
pgaftest: add help command, show subcommands, step env fallback, PGAF…
dimitri Jul 15, 2026
72e7dc7
pgaftest: add show step with markers, auto-advance pgaftest step
dimitri Jul 15, 2026
c911f25
pgaftest: fix option ordering by setting optind=0 in pgaftest_getopts
dimitri Jul 15, 2026
fdd76c1
pgaftest help: use commandline_print_command_tree like pg_autoctl help
dimitri Jul 15, 2026
381d4cd
pgaftest: mount host workDir read-write in pgaftest service container
dimitri Jul 15, 2026
fd9f4cb
pgaftest: run pgaftest service as docker user, deploy in its HOME
dimitri Jul 15, 2026
50c206f
docs: rewrite pgaftest.rst — host vs container commands, DooD, state …
dimitri Jul 15, 2026
423df66
pgaftest: write state file to $HOME inside container, workDir on host
dimitri Jul 15, 2026
1fb27b0
pgaftest: restructure CLI — cluster subcommand, tmux command, show st…
dimitri Jul 15, 2026
6c09933
pgaftest: rename PGAFTEST_COMPOSE_SERVICE to PGAFTEST_IN_CONTAINER
dimitri Jul 15, 2026
a597cda
pg_autoctl watch --wait: retry connecting to the monitor until it's r…
dimitri Jul 15, 2026
78b8332
build: enforce -Werror; fix tmux bottom pane disappearing on containe…
dimitri Jul 15, 2026
4a7073e
pgaftest cluster sh: open a bash shell in the pgaftest container
dimitri Jul 15, 2026
c2c9353
pgaftest tmux: switch from exec to docker compose run; 4-pane layout
dimitri Jul 15, 2026
cbdddac
pgaftest: default --work-dir from last started cluster ($TMPDIR/pgaft…
dimitri Jul 15, 2026
7da89bb
pgaftest cluster down: kill tmux session on teardown
dimitri Jul 15, 2026
ec7a11c
pgaftest: grant docker socket access to container user via group_add
dimitri Jul 15, 2026
e68c556
pgaftest tmux: name containers, check tmux exists, fix socket access
dimitri Jul 15, 2026
0d84580
fix: replace log_notice (nonexistent) with log_debug for tmux version
dimitri Jul 15, 2026
9bee339
pgaftest: rename interactive service to 'setup', use profiles to excl…
dimitri Jul 15, 2026
243af2b
pgaftest tmux: skip runner_wait_for_monitor (docker compose up alread…
dimitri Jul 15, 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
379 changes: 277 additions & 102 deletions docs/ref/pgaftest.rst

Large diffs are not rendered by default.

16 changes: 11 additions & 5 deletions src/bin/Makefile
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the PostgreSQL License.

all: common pg_autoctl pgaftest ;
COMMON_LIB = common/libpgaf_common.a

common:
all: pg_autoctl pgaftest ;

# Build the shared archive once, serially, before the two binaries run in
# parallel. Both pg_autoctl and pgaftest include Makefile.common which
# defines compile rules for common/*.c; without this serialisation a
# parallel make -j would race to write the same .o files simultaneously.
$(COMMON_LIB):
$(MAKE) -C common

pg_autoctl: common
pg_autoctl: $(COMMON_LIB)
$(MAKE) -C pg_autoctl pg_autoctl

pgaftest: common
pgaftest: $(COMMON_LIB)
$(MAKE) -C pgaftest pgaftest

clean:
Expand All @@ -21,4 +27,4 @@ install: pg_autoctl pgaftest
$(MAKE) -C pg_autoctl install
$(MAKE) -C pgaftest install

.PHONY: all common pg_autoctl pgaftest install clean
.PHONY: all pg_autoctl pgaftest install clean
4 changes: 1 addition & 3 deletions src/bin/common/Makefile.common
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ DEFAULT_CFLAGS += -I $(shell $(PG_CONFIG) --pkgincludedir)/internal
DEFAULT_CFLAGS += $(shell $(PG_CONFIG) --cflags)
DEFAULT_CFLAGS += -Wformat
DEFAULT_CFLAGS += -Wall
DEFAULT_CFLAGS += -Werror=implicit-int
DEFAULT_CFLAGS += -Werror=implicit-function-declaration
DEFAULT_CFLAGS += -Werror=return-type
DEFAULT_CFLAGS += -Werror
DEFAULT_CFLAGS += -Wno-declaration-after-statement
DEFAULT_CFLAGS += -D_WANT_SEMUN
DEFAULT_CFLAGS += -Wno-missing-braces
Expand Down
8 changes: 8 additions & 0 deletions src/bin/pg_autoctl/cli_common.c
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,14 @@ cli_common_keeper_getopts(int argc, char **argv,
break;
}

case 'G':
{
/* { "region", required_argument, NULL, 'G' } */
strlcpy(LocalOptionConfig.region, optarg, NAMEDATALEN);
log_trace("--region %s", LocalOptionConfig.region);
break;
}

case 'V':
{
/* keeper_cli_print_version prints version and exits. */
Expand Down
9 changes: 6 additions & 3 deletions src/bin/pg_autoctl/cli_create_node.c
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ cli_create_postgres_getopts(int argc, char **argv)
{ "candidate-priority", required_argument, NULL, 'P' },
{ "replication-quorum", required_argument, NULL, 'r' },
{ "maximum-backup-rate", required_argument, NULL, 'R' },
{ "region", required_argument, NULL, 'G' },
{ "run", no_argument, NULL, 'x' },
{ "no-ssl", no_argument, NULL, 'N' },
{ "ssl-self-signed", no_argument, NULL, 's' },
Expand All @@ -356,7 +357,7 @@ cli_create_postgres_getopts(int argc, char **argv)

int optind =
cli_create_node_getopts(argc, argv, long_options,
"C:D:H:p:l:U:A:SLd:a:n:f:m:MI:W:w:RVvqhP:r:xsN",
"C:D:H:p:l:U:A:SLd:a:n:f:m:MI:W:w:RGVvqhP:r:xsN",
&options);

/* publish our option parsing in the global variable */
Expand Down Expand Up @@ -441,6 +442,7 @@ cli_create_coordinator_getopts(int argc, char **argv)
{ "citus-cluster", required_argument, NULL, 'Z' },
{ "candidate-priority", required_argument, NULL, 'P' },
{ "replication-quorum", required_argument, NULL, 'r' },
{ "region", required_argument, NULL, 'G' },
{ "run", no_argument, NULL, 'x' },
{ "no-ssl", no_argument, NULL, 'N' },
{ "ssl-self-signed", no_argument, NULL, 's' },
Expand All @@ -454,7 +456,7 @@ cli_create_coordinator_getopts(int argc, char **argv)

int optind =
cli_create_node_getopts(argc, argv, long_options,
"C:D:H:p:l:U:A:SLd:a:n:f:m:MRVvqhzZ:P:r:xsN",
"C:D:H:p:l:U:A:SLd:a:n:f:m:MRGVvqhzZ:P:r:xsN",
&options);

options.groupId = 0;
Expand Down Expand Up @@ -551,6 +553,7 @@ cli_create_worker_getopts(int argc, char **argv)
{ "citus-cluster", required_argument, NULL, 'Z' },
{ "candidate-priority", required_argument, NULL, 'P' },
{ "replication-quorum", required_argument, NULL, 'r' },
{ "region", required_argument, NULL, 'G' },
{ "run", no_argument, NULL, 'x' },
{ "no-ssl", no_argument, NULL, 'N' },
{ "ssl-self-signed", no_argument, NULL, 's' },
Expand All @@ -564,7 +567,7 @@ cli_create_worker_getopts(int argc, char **argv)

int optind =
cli_create_node_getopts(argc, argv, long_options,
"C:D:H:p:l:y:zZ:U:A:SLd:a:n:f:m:MRVvqhzP:r:xsN",
"C:D:H:p:l:y:zZ:U:A:SLd:a:n:f:m:MRGVvqhzP:r:xsN",
&options);

if (options.groupId == 0)
Expand Down
21 changes: 19 additions & 2 deletions src/bin/pg_autoctl/cli_watch.c
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
static int cli_watch_getopts(int argc, char **argv);
static void cli_watch(int argc, char **argv);

static bool watchWaitForMonitor = false;

CommandLine watch_command =
make_command("watch",
"Display a dashboard to watch monitor's events and state",
Expand All @@ -51,7 +53,8 @@ CommandLine watch_command =
" --monitor show the monitor uri\n"
" --formation formation to query, defaults to 'default' \n"
" --group group to query formation, defaults to all \n"
" --json output data in the JSON format\n",
" --json output data in the JSON format\n"
" --wait wait for the monitor to be ready before starting\n",
cli_watch_getopts,
cli_watch);

Expand All @@ -67,6 +70,7 @@ cli_watch_getopts(int argc, char **argv)
{ "monitor", required_argument, NULL, 'm' },
{ "formation", required_argument, NULL, 'f' },
{ "group", required_argument, NULL, 'g' },
{ "wait", no_argument, NULL, 'w' },
{ "version", no_argument, NULL, 'V' },
{ "verbose", no_argument, NULL, 'v' },
{ "quiet", no_argument, NULL, 'q' },
Expand All @@ -84,7 +88,7 @@ cli_watch_getopts(int argc, char **argv)

optind = 0;

while ((c = getopt_long(argc, argv, "D:f:g:n:Vvqh",
while ((c = getopt_long(argc, argv, "D:f:g:n:wVvqh",
long_options, &option_index)) != -1)
{
switch (c)
Expand Down Expand Up @@ -128,6 +132,13 @@ cli_watch_getopts(int argc, char **argv)
break;
}

case 'w':
{
watchWaitForMonitor = true;
log_trace("--wait");
break;
}

case 'V':
{
/* keeper_cli_print_version prints version and exits. */
Expand Down Expand Up @@ -238,6 +249,12 @@ cli_watch(int argc, char **argv)

(void) cli_monitor_init_from_option_or_config(&(context.monitor), &config);

if (watchWaitForMonitor)
{
pgsql_set_monitor_interactive_retry_policy(
&(context.monitor.pgsql.retryPolicy));
}

strlcpy(context.formation, config.formation, sizeof(context.formation));
context.groupId = config.groupId;

Expand Down
2 changes: 1 addition & 1 deletion src/bin/pg_autoctl/defaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
#define PG_AUTOCTL_VERSION GIT_VERSION

/* version of the extension that we requite to talk to on the monitor */
#define PG_AUTOCTL_EXTENSION_VERSION "2.2"
#define PG_AUTOCTL_EXTENSION_VERSION "2.3"

/* environment variable to use to make DEBUG facilities available */
#define PG_AUTOCTL_DEBUG "PG_AUTOCTL_DEBUG"
Expand Down
2 changes: 2 additions & 0 deletions src/bin/pg_autoctl/keeper.c
Original file line number Diff line number Diff line change
Expand Up @@ -1670,6 +1670,7 @@ keeper_register_and_init(Keeper *keeper, NodeState initialState)
config->pgSetup.settings.candidatePriority,
config->pgSetup.settings.replicationQuorum,
config->pgSetup.citusClusterName,
config->region,
&mayRetry,
&assignedState))
{
Expand Down Expand Up @@ -1875,6 +1876,7 @@ keeper_register_again(Keeper *keeper)
config->pgSetup.settings.candidatePriority,
config->pgSetup.settings.replicationQuorum,
DEFAULT_CITUS_CLUSTER_NAME,
config->region,
&mayRetry,
&assignedState))
{
Expand Down
5 changes: 5 additions & 0 deletions src/bin/pg_autoctl/keeper_config.c
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
make_strbuf_option("pg_autoctl", "nodekind", NULL, false, NAMEDATALEN, \
config->nodeKind)

#define OPTION_AUTOCTL_REGION(config) \
make_strbuf_option_default("pg_autoctl", "region", "region", \
false, NAMEDATALEN, config->region, "")

#define OPTION_POSTGRESQL_PGDATA(config) \
make_strbuf_option("postgresql", "pgdata", "pgdata", true, MAXPGPATH, \
config->pgSetup.pgdata)
Expand Down Expand Up @@ -227,6 +231,7 @@
OPTION_AUTOCTL_HOSTNAME(config), \
OPTION_AUTOCTL_NODENAME(config), \
OPTION_AUTOCTL_NODEKIND(config), \
OPTION_AUTOCTL_REGION(config), \
OPTION_POSTGRESQL_PGDATA(config), \
OPTION_POSTGRESQL_PG_CTL(config), \
OPTION_POSTGRESQL_USERNAME(config), \
Expand Down
1 change: 1 addition & 0 deletions src/bin/pg_autoctl/keeper_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ typedef struct KeeperConfig
char name[_POSIX_HOST_NAME_MAX];
char hostname[_POSIX_HOST_NAME_MAX];
char nodeKind[NAMEDATALEN];
char region[NAMEDATALEN];

/* PostgreSQL setup */
PostgresSetup pgSetup;
Expand Down
44 changes: 30 additions & 14 deletions src/bin/pg_autoctl/monitor.c
Original file line number Diff line number Diff line change
Expand Up @@ -851,19 +851,20 @@ monitor_register_node(Monitor *monitor, char *formation,
NodeState initialState,
PgInstanceKind kind, int candidatePriority, bool quorum,
char *citusClusterName,
char *region,
bool *mayRetry,
MonitorAssignedState *assignedState)
{
PGSQL *pgsql = &monitor->pgsql;
const char *sql =
"SELECT * FROM pgautofailover.register_node($1, $2, $3, $4, $5, $6, $7, "
"$8, $9::pgautofailover.replication_state, $10, $11, $12, $13)";
int paramCount = 13;
Oid paramTypes[13] = {
"$8, $9::pgautofailover.replication_state, $10, $11, $12, $13, $14)";
int paramCount = 14;
Oid paramTypes[14] = {
TEXTOID, TEXTOID, INT4OID, NAMEOID, TEXTOID, INT8OID,
INT8OID, INT4OID, TEXTOID, TEXTOID, INT4OID, BOOLOID, TEXTOID
INT8OID, INT4OID, TEXTOID, TEXTOID, INT4OID, BOOLOID, TEXTOID, TEXTOID
};
const char *paramValues[13];
const char *paramValues[14];
MonitorAssignedStateParseContext parseContext =
{ { 0 }, assignedState, false };
const char *nodeStateString = NodeStateToString(initialState);
Expand All @@ -889,6 +890,10 @@ monitor_register_node(Monitor *monitor, char *formation,
IS_EMPTY_STRING_BUFFER(citusClusterName)
? DEFAULT_CITUS_CLUSTER_NAME
: citusClusterName;
paramValues[13] =
IS_EMPTY_STRING_BUFFER(region)
? "default"
: region;

if (!pgsql_execute_with_params(pgsql, sql,
paramCount, paramTypes, paramValues,
Expand Down Expand Up @@ -1940,7 +1945,7 @@ monitor_get_current_state(Monitor *monitor, char *formation, int group,
" current_group_state, assigned_group_state, "
" candidate_priority, replication_quorum, "
" reported_tli, reported_lsn, health, nodecluster, "
" healthlag, reportlag"
" noderegion, healthlag, reportlag"
" FROM pgautofailover.current_state($1) cs "
" JOIN ("
" select nodeid, "
Expand All @@ -1966,7 +1971,7 @@ monitor_get_current_state(Monitor *monitor, char *formation, int group,
" current_group_state, assigned_group_state, "
" candidate_priority, replication_quorum, "
" reported_tli, reported_lsn, health, nodecluster, "
" healthlag, reportlag"
" noderegion, healthlag, reportlag"
" FROM pgautofailover.current_state($1, $2) cs "
" JOIN ("
" select nodeid, "
Expand Down Expand Up @@ -2019,7 +2024,7 @@ parseCurrentNodeState(PGresult *result, int rowNumber,
int errors = 0;

/* we don't expect any of the column to be NULL */
for (colNumber = 0; colNumber < 16; colNumber++)
for (colNumber = 0; colNumber < 17; colNumber++)
{
if (PQgetisnull(result, rowNumber, 0))
{
Expand All @@ -2044,8 +2049,9 @@ parseCurrentNodeState(PGresult *result, int rowNumber,
* 11 - OUT reported_lsn pg_lsn,
* 12 - OUT health integer
* 13 - OUT nodecluster text
* 14 - healthlag int (extract epoch from interval)
* 15 - reportlag int (extract epoch from interval)
* 14 - OUT noderegion text
* 15 - healthlag int (extract epoch from interval)
* 16 - reportlag int (extract epoch from interval)
*
* We need the groupId to parse the formation kind into a nodeKind, so we
* begin at column 1 and get back to column 0 later, after column 4.
Expand Down Expand Up @@ -2181,14 +2187,24 @@ parseCurrentNodeState(PGresult *result, int rowNumber,
}

value = PQgetvalue(result, rowNumber, 14);
length = strlcpy(nodeState->region, value, NAMEDATALEN);
if (length >= NAMEDATALEN)
{
log_error("Region \"%s\" returned by monitor is %d characters, "
"the maximum supported by pg_autoctl is %d",
value, length, NAMEDATALEN - 1);
++errors;
}

value = PQgetvalue(result, rowNumber, 15);

if (!stringToDouble(value, &(nodeState->healthLag)))
{
log_error("Invalid health lag \"%s\" returned by monitor", value);
++errors;
}

value = PQgetvalue(result, rowNumber, 15);
value = PQgetvalue(result, rowNumber, 16);

if (!stringToDouble(value, &(nodeState->reportLag)))
{
Expand Down Expand Up @@ -2221,10 +2237,10 @@ parseCurrentNodeStateArray(CurrentNodeStateArray *nodesArray, PGresult *result)
return false;
}

/* pgautofailover.current_state returns 11 columns */
if (PQnfields(result) != 16)
/* pgautofailover.current_state returns 17 columns (including noderegion) */
if (PQnfields(result) != 17)
{
log_error("Query returned %d columns, expected 16", PQnfields(result));
log_error("Query returned %d columns, expected 17", PQnfields(result));
return false;
}

Expand Down
1 change: 1 addition & 0 deletions src/bin/pg_autoctl/monitor.h
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ bool monitor_register_node(Monitor *monitor,
int candidatePriority,
bool quorum,
char *citusClusterName,
char *region,
bool *mayRetry,
MonitorAssignedState *assignedState);
bool monitor_node_active(Monitor *monitor,
Expand Down
10 changes: 10 additions & 0 deletions src/bin/pg_autoctl/nodespec.c
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ nodespec_read(const char *path, NodeSpec *spec)
make_strbuf_option_default("settings", "replication_quorum", NULL, false,
sizeof(replicationQuorumStr),
replicationQuorumStr, "true"),
make_strbuf_option_default("settings", "region", NULL, false,
sizeof(spec->region), spec->region,
""),

/* [options] — immutable, used only at create time */
make_strbuf_option_default("options", "ssl", NULL, false,
Expand Down Expand Up @@ -674,6 +677,13 @@ nodespec_create_argv(const NodeSpec *spec,
PUSH("false");
}

/* region label (omit when empty — monitor defaults to "default") */
if (!IS_EMPTY_STRING_BUFFER(spec->region))
{
PUSH("--region");
PUSH(spec->region);
}

/* Citus secondary/read-replica cluster settings */
if (spec->citusSecondary)
{
Expand Down
1 change: 1 addition & 0 deletions src/bin/pg_autoctl/nodespec.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ typedef struct NodeSpec
/* [settings] — mutable; applied on SIGHUP / file change */
int candidate_priority; /* 0-100, default 50 */
bool replication_quorum; /* sync quorum participant, default true */
char region[NAMEDATALEN]; /* data-centre / availability zone label */

/* [options] — immutable; used only at pg_autoctl create time */
char ssl[32]; /* self-signed | verify-ca | verify-full | off */
Expand Down
1 change: 1 addition & 0 deletions src/bin/pg_autoctl/nodestate_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ typedef struct CurrentNodeState

char formation[NAMEDATALEN];
char citusClusterName[NAMEDATALEN];
char region[NAMEDATALEN];
int groupId;
PgInstanceKind pgKind;

Expand Down
Loading
Loading