Skip to content

Commit 48c02be

Browse files
authored
keeper: send node_active reports to monitor during graceful shutdown (#1146)
* keeper: send node_active reports to monitor during graceful shutdown On SIGTERM, the node-active service's main loop exits immediately, before calling keeper_update_pg_state() or service_keeper_node_active(). This leaves the monitor unaware that node1 is going offline until its next health-check poll, causing the flaky behaviour seen in ensure::test_004_demoted where node2 never reached wait_primary within the test timeout. Add keeper_node_active_shutdown_loop(): after the main loop breaks on asked_to_stop, report the current Postgres state to the monitor every 1s for up to 30 seconds (or until Postgres stops or an escalated signal arrives). This runs concurrently with the postgres-controller service calling 'pg_ctl stop -m fast': fast mode sends SIGTERM to the postmaster, which stops accepting new connections immediately, while the checkpoint completes asynchronously. By the time the first shutdown report fires, Postgres is already refusing connections; the monitor can start failover right away rather than waiting for a health-check timeout. SIGINT (asked_to_stop_fast) and SIGQUIT (asked_to_quit) skip the shutdown loop and exit immediately, as before. * fix ensure::test_003: re-enable auto-start after stop postgres test_003_init_secondary stops postgres on node2 then waits for it to recover as secondary. The original Python test (node2.stop_postgres()) sends SIGTERM directly to postgres without touching pg_autoctl's service controller, so the keeper auto-restarts postgres. The pgaftest port used 'stop postgres node2' which calls 'pg_autoctl manual service pgctl off' — this both stops postgres AND disables auto-restart. As a result node2's postgres stayed stopped for the rest of the spec. In test_004_demoted, NodeIsHealthy(node2) checks pgIsRunning, which was false because node2's postgres was never restarted. The failover transition at group_state_machine.c:550 requires NodeIsHealthy(activeNode) to be true, so the failover never triggered and the test timed out at 180s. Fix: add 'start postgres node2' immediately after 'stop postgres node2' to call 'pg_autoctl manual service pgctl on', re-enabling the keeper's auto-restart. The keeper restarts postgres and node2 converges back to secondary before test_004 runs.
1 parent 53a1a9d commit 48c02be

2 files changed

Lines changed: 71 additions & 0 deletions

File tree

src/bin/pg_autoctl/service_keeper.c

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,61 @@ service_keeper_node_active_init(Keeper *keeper)
244244
}
245245

246246

247+
/*
248+
* keeper_node_active_shutdown_loop sends node_active reports to the monitor
249+
* every second for up to KEEPER_SHUTDOWN_LOOP_MAX_SECS while PostgreSQL stops.
250+
*
251+
* This runs after the main node-active loop exits on SIGTERM, concurrently
252+
* with the postgres-controller service (a sibling process) calling
253+
* "pg_ctl stop -m fast".
254+
*
255+
* When SIGTERM reaches the postmaster, process_pm_shutdown_request()
256+
* (src/backend/postmaster/postmaster.c) sets Shutdown = FastShutdown and
257+
* calls UpdatePMState(PM_STOP_BACKENDS). After that transition,
258+
* canAcceptConnections() (same file) returns CAC_SHUTDOWN for every new
259+
* connection attempt, so the primary stops accepting writes immediately —
260+
* before a single backend has rolled back. Only the final checkpoint that
261+
* follows can be slow.
262+
*
263+
* By continuing to call node_active with the current pgIsRunning value here,
264+
* we ensure the monitor learns the primary is going away within one second of
265+
* the shutdown starting, and can begin failover right away rather than waiting
266+
* for a health-check timeout.
267+
*/
268+
#define KEEPER_SHUTDOWN_LOOP_MAX_SECS 30
269+
270+
static void
271+
keeper_node_active_shutdown_loop(Keeper *keeper)
272+
{
273+
LocalPostgresServer *postgres = &(keeper->postgres);
274+
275+
log_info("Graceful shutdown: reporting node state to monitor "
276+
"while PostgreSQL stops (up to %d seconds)",
277+
KEEPER_SHUTDOWN_LOOP_MAX_SECS);
278+
279+
for (int i = 0; i < KEEPER_SHUTDOWN_LOOP_MAX_SECS; i++)
280+
{
281+
/* escalated signal: exit without further reporting */
282+
if (asked_to_quit || asked_to_stop_fast)
283+
{
284+
break;
285+
}
286+
287+
(void) keeper_update_pg_state(keeper, LOG_DEBUG);
288+
(void) service_keeper_node_active(keeper, false);
289+
290+
if (!postgres->pgIsRunning)
291+
{
292+
log_info("PostgreSQL has stopped; "
293+
"final node_active report sent to monitor");
294+
break;
295+
}
296+
297+
pg_usleep(1000000L); /* 1 second */
298+
}
299+
}
300+
301+
247302
/*
248303
* keeper_node_active_loop implements the main loop of the keeper, which
249304
* periodically gets the goal state from the monitor and makes the state
@@ -659,6 +714,16 @@ keeper_node_active_loop(Keeper *keeper, pid_t start_pid)
659714
}
660715
}
661716

717+
/*
718+
* Graceful SIGTERM shutdown: keep reporting state to the monitor while
719+
* PostgreSQL finishes its checkpoint and stops. Skip on SIGINT/SIGQUIT
720+
* which request immediate exit.
721+
*/
722+
if (asked_to_stop && !asked_to_stop_fast && !asked_to_quit)
723+
{
724+
(void) keeper_node_active_shutdown_loop(keeper);
725+
}
726+
662727
/* One last check that we do not have any connections open */
663728
pgsql_finish(&(keeper->monitor.pgsql));
664729
pgsql_finish(&(monitor->notificationClient));

tests/tap/specs/ensure.pgaf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,13 @@ step test_003_init_secondary {
4141
wait until node2 state is secondary
4242
and node1 state is primary
4343
timeout 90s
44+
# Stop postgres while pg_autoctl is running, then re-enable auto-start so
45+
# the keeper restarts postgres automatically. This tests that the keeper
46+
# recovers from an unexpected postgres stop. The Python equivalent is
47+
# node2.stop_postgres() which sends SIGTERM to postgres without disabling
48+
# the pg_autoctl postgres-controller restart logic.
4449
stop postgres node2
50+
start postgres node2
4551
wait until node2 state is secondary
4652
and node1 state is primary
4753
timeout 90s

0 commit comments

Comments
 (0)