Skip to content

Commit 53a1a9d

Browse files
authored
Monitor: detect and recover from fast_forward candidate stuck with all WAL sources unhealthy (#1143)
* Fix fast_forward self-reference infinite loop When a node is assigned the fast_forward goalstate it has not yet reported that state back to the monitor, so reportedstate is still report_lsn. If the originally-selected upstream peer transitions out of report_lsn before the fast_forward node calls get_most_advanced_standby(), the fast_forward node becomes the only remaining report_lsn node and is returned as its own upstream source. The node then loops forever trying to fetch WAL from itself. Fix across three layers: SQL (get_most_advanced_standby): add caller_node_id bigint parameter (default 0) and filter AND nodeid != $3 so the calling node can never be returned as its own upstream. monitor_get_most_advanced_standby: accept callerNodeId and a bool *found output parameter. A zero-row result is no longer an error: it means the caller is already the most advanced node and the found flag is set to false. keeper_get_most_advanced_standby: pass the local node ID to the monitor call; also skip self in the no-monitor path. Propagates the found flag to the caller. fsm_fast_forward: when found is false (no valid upstream), skip the WAL fetch and return true so the keeper can report its current state to the monitor. The monitor will then assign prepare_promotion on the next node_active call, breaking the loop. fsm_init_from_standby also uses keeper_get_most_advanced_standby; there a not-found result is a genuine error (no source to clone from) so it returns false with an explicit log message. Fixes #1060 * Monitor: detect fast_forward candidate stuck with all WAL sources unhealthy When a failover candidate is assigned fast_forward (its LSN lags behind a peer standby that holds more WAL), it fetches the missing WAL before promotion. If the WAL-source node(s) die while the candidate is fetching, the candidate gets stuck: it reports back report_lsn (failed fetch) while its goal stays fast_forward. IsBeingPromoted() holds the lock, so the election cannot restart, and ProceedWithMSFailover does nothing useful because CandidateNodeIsReadyToStreamWAL explicitly excludes fast_forward. Fix (group_state_machine.c): Add WalSourceNodesAreAllUnhealthy() helper that scans the group for nodes in {report_lsn, report_lsn} (the WAL-source state) whose health is bad. When this fires for a candidate in {report_lsn, fast_forward} inside ProceedGroupStateForMSFailover, the monitor acts based on the guard_data_loss GUC: - guard_data_loss=true (default): reset the candidate goal back to report_lsn and WARN the operator. The election retries automatically if a source recovers. Operators can unblock with: pg_autoctl perform failover --allow-data-loss - guard_data_loss=false: log and fall through. get_most_advanced_standby() now filters out unhealthy nodes when guard_data_loss=false (SQL change below), so fsm_fast_forward() finds no upstream, skips the WAL fetch, and reports fast_forward as current. The monitor then assigns prepare_promotion on the next call. Fix (pgautofailover.sql): get_most_advanced_standby() adds: AND (current_setting('pgautofailover.guard_data_loss')::bool OR health > 0) When guard_data_loss=false, unhealthy nodes are excluded from WAL-source selection. When guard_data_loss=true, the highest-LSN node is returned regardless of health (preserving the no-data-loss guarantee). Tests: src/monitor/sql/fast_forward.sql - regression test: bootstraps a 3-node formation, manually places the candidate in {report_lsn, fast_forward} with the WAL source unhealthy, then: Test A (guard_data_loss=true): node_active returns report_lsn ✓ Test B (guard_data_loss=false): node_active returns fast_forward ✓ Also calls get_most_advanced_standby() directly to exercise both SQL filter paths. tests/tap/specs/fast_forward.pgaf - integration test: kills primary and WAL-source standby together, verifies node stays at report_lsn, then unblocks with --allow-data-loss and verifies full cluster recovery. src/monitor/Makefile and tests/tap/schedule updated accordingly. Fixes #1060
1 parent ca7834b commit 53a1a9d

12 files changed

Lines changed: 961 additions & 21 deletions

File tree

‎src/bin/pg_autoctl/fsm_transition.c‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1320,17 +1320,31 @@ fsm_fast_forward(Keeper *keeper)
13201320
ReplicationSource *upstream = &(postgres->replicationSource);
13211321

13221322
NodeAddress upstreamNode = { 0 };
1323+
bool found = false;
13231324

13241325
char slotName[MAXCONNINFO] = { 0 };
13251326

1326-
/* get the primary node to follow */
1327-
if (!keeper_get_most_advanced_standby(keeper, &upstreamNode))
1327+
/* get the most advanced peer standby to fetch missing WAL from */
1328+
if (!keeper_get_most_advanced_standby(keeper, &upstreamNode, &found))
13281329
{
13291330
log_error("Failed to fast forward from the most advanced standby node, "
13301331
"see above for details");
13311332
return false;
13321333
}
13331334

1335+
/*
1336+
* When no other report_lsn peer exists (because the intended upstream
1337+
* already transitioned away), this node is now the most advanced.
1338+
* Skip the WAL fetch — the monitor will assign prepare_promotion on the
1339+
* next node_active call.
1340+
*/
1341+
if (!found)
1342+
{
1343+
log_info("No upstream standby found for fast_forward; "
1344+
"skipping WAL fetch and proceeding to promotion");
1345+
return true;
1346+
}
1347+
13341348
/*
13351349
* Postgres 10 does not have pg_replication_slot_advance(), so we don't
13361350
* support replication slots on standby nodes there.
@@ -1471,15 +1485,23 @@ fsm_init_from_standby(Keeper *keeper)
14711485
LocalPostgresServer *postgres = &(keeper->postgres);
14721486

14731487
NodeAddress upstreamNode = { 0 };
1488+
bool found = false;
14741489

14751490
/* get the primary node to follow */
1476-
if (!keeper_get_most_advanced_standby(keeper, &upstreamNode))
1491+
if (!keeper_get_most_advanced_standby(keeper, &upstreamNode, &found))
14771492
{
14781493
log_error("Failed to initialise from the most advanced standby node, "
14791494
"see above for details");
14801495
return false;
14811496
}
14821497

1498+
if (!found)
1499+
{
1500+
log_error("No standby node found to initialise from; "
1501+
"cannot proceed without an upstream source");
1502+
return false;
1503+
}
1504+
14831505
if (!standby_init_replication_source(postgres,
14841506
&upstreamNode,
14851507
PG_AUTOCTL_REPLICA_USERNAME,

‎src/bin/pg_autoctl/keeper.c‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3109,10 +3109,12 @@ keeper_get_primary(Keeper *keeper, NodeAddress *primaryNode)
31093109
* the keeper->otherNodes array.
31103110
*/
31113111
bool
3112-
keeper_get_most_advanced_standby(Keeper *keeper, NodeAddress *upstreamNode)
3112+
keeper_get_most_advanced_standby(Keeper *keeper, NodeAddress *upstreamNode,
3113+
bool *found)
31133114
{
31143115
KeeperConfig *config = &(keeper->config);
31153116
int groupId = keeper->state.current_group;
3117+
int64_t localNodeId = keeper->state.current_node_id;
31163118

31173119
if (!config->monitorDisabled)
31183120
{
@@ -3121,7 +3123,9 @@ keeper_get_most_advanced_standby(Keeper *keeper, NodeAddress *upstreamNode)
31213123
if (!monitor_get_most_advanced_standby(monitor,
31223124
config->formation,
31233125
groupId,
3124-
upstreamNode))
3126+
localNodeId,
3127+
upstreamNode,
3128+
found))
31253129
{
31263130
log_error("Failed to get the most advanced standby node "
31273131
"from the monitor, see above for details");
@@ -3140,6 +3144,12 @@ keeper_get_most_advanced_standby(Keeper *keeper, NodeAddress *upstreamNode)
31403144
NodeAddress *node = &(keeper->otherNodes.nodes[i]);
31413145
uint64_t nodeLSN = 0;
31423146

3147+
/* skip self to avoid fetching WAL from ourselves */
3148+
if (node->nodeId == localNodeId)
3149+
{
3150+
continue;
3151+
}
3152+
31433153
if (!parseLSN(node->lsn, &nodeLSN))
31443154
{
31453155
log_error("Failed to parse node %" PRId64
@@ -3158,14 +3168,14 @@ keeper_get_most_advanced_standby(Keeper *keeper, NodeAddress *upstreamNode)
31583168

31593169
if (mostAdvandedStandbyNode == NULL)
31603170
{
3161-
log_error("Failed to get the most avdanced standby node "
3162-
"from the current list of other nodes, "
3163-
"refresh the list with the command: "
3164-
"pg_autoctl do fsm nodes set");
3165-
return false;
3171+
log_info("No other standby found in local node list; "
3172+
"node %" PRId64 " is the most advanced", localNodeId);
3173+
*found = false;
3174+
return true;
31663175
}
31673176

31683177
*upstreamNode = *mostAdvandedStandbyNode;
3178+
*found = true;
31693179
return true;
31703180
}
31713181

‎src/bin/pg_autoctl/keeper.h‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ bool keeper_refresh_citus_remove_dropped_nodes(Keeper *keeper,
120120

121121
bool keeper_read_nodes_from_file(Keeper *keeper, NodeAddressArray *nodesArray);
122122
bool keeper_get_primary(Keeper *keeper, NodeAddress *primaryNode);
123-
bool keeper_get_most_advanced_standby(Keeper *keeper, NodeAddress *primaryNode);
123+
bool keeper_get_most_advanced_standby(Keeper *keeper, NodeAddress *primaryNode,
124+
bool *found);
124125

125126

126127
bool keeper_pg_autoctl_get_version_from_disk(Keeper *keeper,

‎src/bin/pg_autoctl/monitor.c‎

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -751,23 +751,26 @@ monitor_get_coordinator(Monitor *monitor, char *formation,
751751
bool
752752
monitor_get_most_advanced_standby(Monitor *monitor,
753753
char *formation, int groupId,
754-
NodeAddress *node)
754+
int64_t callerNodeId,
755+
NodeAddress *node, bool *found)
755756
{
756757
PGSQL *pgsql = &monitor->pgsql;
757758
const char *sql =
758-
"SELECT * FROM pgautofailover.get_most_advanced_standby($1, $2)";
759-
int paramCount = 2;
760-
Oid paramTypes[2] = { TEXTOID, INT4OID };
761-
const char *paramValues[2];
759+
"SELECT * FROM pgautofailover.get_most_advanced_standby($1, $2, $3)";
760+
int paramCount = 3;
761+
Oid paramTypes[3] = { TEXTOID, INT4OID, INT8OID };
762+
const char *paramValues[3];
762763

763-
/* we expect a single entry */
764+
/* we expect zero or one entry */
764765
NodeAddressArray nodeArray = { 0 };
765766
NodeAddressArrayParseContext parseContext = { { 0 }, &nodeArray, false };
766767

767768
IntString groupIdString = intToString(groupId);
769+
IntString callerNodeIdString = intToString(callerNodeId);
768770

769771
paramValues[0] = formation;
770772
paramValues[1] = groupIdString.strValue;
773+
paramValues[2] = callerNodeIdString.strValue;
771774

772775
if (!pgsql_execute_with_params(pgsql, sql,
773776
paramCount, paramTypes, paramValues,
@@ -781,7 +784,7 @@ monitor_get_most_advanced_standby(Monitor *monitor,
781784
return false;
782785
}
783786

784-
if (!parseContext.parsedOK || nodeArray.count != 1)
787+
if (!parseContext.parsedOK)
785788
{
786789
log_error(
787790
"Failed to get the most advanced standby node from the monitor "
@@ -792,6 +795,15 @@ monitor_get_most_advanced_standby(Monitor *monitor,
792795
return false;
793796
}
794797

798+
/* zero rows: no other report_lsn peer exists; caller is most advanced */
799+
if (nodeArray.count == 0)
800+
{
801+
log_info("No other standby is reporting its LSN; "
802+
"node %" PRId64 " is the most advanced", callerNodeId);
803+
*found = false;
804+
return true;
805+
}
806+
795807
/* copy the node we retrieved in the expected place */
796808
node->nodeId = nodeArray.nodes[0].nodeId;
797809
strlcpy(node->name, nodeArray.nodes[0].name, _POSIX_HOST_NAME_MAX);
@@ -803,6 +815,7 @@ monitor_get_most_advanced_standby(Monitor *monitor,
803815
log_debug("The most advanced standby node is node " NODE_FORMAT,
804816
node->nodeId, node->name, node->host, node->port);
805817

818+
*found = true;
806819
return true;
807820
}
808821

‎src/bin/pg_autoctl/monitor.h‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ bool monitor_get_coordinator(Monitor *monitor, char *formation,
124124
CoordinatorNodeAddress *coordinatorNodeAddress);
125125
bool monitor_get_most_advanced_standby(Monitor *monitor,
126126
char *formation, int groupId,
127-
NodeAddress *node);
127+
int64_t callerNodeId,
128+
NodeAddress *node, bool *found);
128129
bool monitor_register_node(Monitor *monitor,
129130
char *formation,
130131
char *name,

‎src/monitor/Makefile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ MODULE_big = $(EXTENSION)
1414
OBJS = $(patsubst ${SRC_DIR}%.c,%.o,$(wildcard ${SRC_DIR}*.c))
1515
PG_CPPFLAGS = -std=c99 -Wall -Werror -Wno-unused-parameter -Iinclude -I$(libpq_srcdir) -g
1616
SHLIB_LINK = $(libpq)
17-
REGRESS = create_extension monitor workers node_active_protocol guard_data_loss dummy_update drop_extension upgrade
17+
REGRESS = create_extension monitor workers node_active_protocol guard_data_loss fast_forward dummy_update drop_extension upgrade
1818

1919
PG_CONFIG ?= pg_config
2020
PGXS = $(shell $(PG_CONFIG) --pgxs)

0 commit comments

Comments
 (0)