Skip to content

pgaftest/pg_autoctl: step-mode tooling (no-autopilot, fsm step, report/advance split) - #1183

Merged
dimitri merged 12 commits into
mainfrom
pgaftest-step-mode-and-report-advance
Aug 2, 2026
Merged

pgaftest/pg_autoctl: step-mode tooling (no-autopilot, fsm step, report/advance split)#1183
dimitri merged 12 commits into
mainfrom
pgaftest-step-mode-and-report-advance

Conversation

@dimitri

@dimitri dimitri commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Summary

pg_autoctl's node-active service normally reports a node's current state
to the monitor and, in the same call, immediately attempts whatever
transition the monitor assigns back — atomically, with no way to observe,
or hold a node at, the moment in between. That's the right default for
normal operation, but it makes some things impossible: reproducing a
specific FSM interleaving in a test, or, for an operator doing manual
recovery, inspecting what the monitor decided before acting on it.

This PR adds the tooling to do that: freeze a node's FSM at a specific
reported state, inspect what the monitor assigns next, and advance one
transition at a time, on demand.

What's in this PR

  • pg_autoctl manual fsm step [report|advance] — splits the existing
    combined manual fsm step into two independently-issuable halves.
    report sends the node's current state to the monitor and persists
    whatever goal state comes back, without attempting the transition.
    advance attempts the transition already on file, without talking to
    the monitor again. Plain step (no argument) is unchanged.

  • Step mode (PG_AUTOCTL_STEP_MODE) — when set, a node's node-active
    service stops ticking its FSM on its own and instead opens a small
    Unix-domain-socket server, waiting to be told exactly when to STEP,
    REPORT, or ADVANCE. pg_autoctl manual fsm step/report/advance
    talk to this socket when it's present, so an external driver gets
    precise, single-step control over an otherwise-live keeper process.

  • no-autopilot — a pgaftest cluster node modifier that starts a
    node with step mode enabled, and fsm step <node> — DSL sugar for
    the combined manual fsm step call against that node, for use in test
    specs that need to drive a node's FSM by hand.

Why this is useful on its own

A no-autopilot node paired with fsm step, report, and advance
lets a test hold a node frozen at a specific reported state while other
nodes in the cluster keep moving, then advance it exactly when the
scenario calls for it — instead of racing a freely-ticking FSM against
sleeps and hoping the timing lines up. The same CLI split is available
directly against a running node for manual, step-by-step recovery outside
of any test.

Proof this works end to end

tests/tap/specs/fsm_step_report_advance.pgaf is a new spec that
exercises the full split live: a no-autopilot node reports a stale
state, the test observes the monitor assign a new goal state without the
node moving, then advance performs that transition, then a follow-up
report confirms the monitor sees the result. Verified locally, 4/4
passing:

ok 1 - test_001_remove_primary_leaving_node2_alone
ok 2 - test_002_node2_reports_and_learns_single
ok 3 - test_003_node2_advances_to_single
ok 4 - test_004_node2_reports_the_new_single_state
# All 4 tests passed.

Registered in tests/tap/schedule and tests/tap/schedules/node.sch.

Docs

  • docs/ref/pgaftest.rst: new "Step mode: no-autopilot nodes" section,
    no-autopilot node-modifier table entry, fsm step <node> DSL command
    reference, test catalog entry.
  • docs/ref/pg_autoctl_manual.rst: fsm step [report|advance] synopsis
    and explanation.

Verification

  • PG_CONFIG=... make -C src/bin/pg_autoctl / make -C src/bin/pgaftest — clean build.
  • make docker-check (citus_indent --check) and ci/banned.h.sh — pass.
  • sphinx-build -b html — no new warnings from either touched doc file.
  • Live Docker run of the new spec (above).

dimitri added 2 commits August 2, 2026 05:05
Introduces PG_AUTOCTL_STEP_MODE, an env var that switches the
persistent node-active service into step mode: instead of ticking on
its own, it blocks on a new Unix-domain control socket
(<pidfile>.step) waiting for explicit "step" commands, and reuses the
existing keeper_fsm_step() to advance the FSM by exactly one
transition per command -- a gdb-step equivalent for the keeper FSM,
useful for reproducing precise race-condition scenarios that outrun
any external SQL-timed pgaftest script.

`pg_autoctl manual fsm step` now detects the control socket and
delegates to it instead of calling keeper_init() itself, which avoids
reintroducing the status-file bug a fresh one-shot CLI process would
otherwise cause (local_postgres_init() unlinks the shared
pg_autoctl.pg file on every keeper_init(), and the persistent postgres
subprocess only rewrites it when it actively needs to (re)start
Postgres).

Verified live in a 2-node Docker cluster: autopilot stays fully
suspended between commands (no reported-state change over repeated
polls despite a real pending transition), each step advances exactly
one FSM edge (maintenance -> catchingup -> secondary), and a crashed
Postgres is restarted on the next step command -- the same recovery
path normal mode uses continuously, just triggered on-demand instead
of every tick, which is the correct semantics for a mode whose whole
point is that nothing happens without an explicit command.

pgaftest's Makefile has its own curated SHARED_SRCS list (not a
wildcard like pg_autoctl's own Makefile), so step_socket.c had to be
added there explicitly for the pgaftest binary to link.
Adds two new .pgaf DSL primitives so a spec can precisely single-step a
node's FSM, the same way this session's earlier PG_AUTOCTL_STEP_MODE
facility (src/bin/pg_autoctl/step_socket.c) already lets an operator do
by hand:

  - a per-node "no-autopilot" cluster modifier (test_spec.h's new
    TestNode.noAutopilot, alongside launchDeferred/noMonitor) that emits
    PG_AUTOCTL_STEP_MODE=1 in that node's compose environment
    (compose_gen.c) -- its node-active service never ticks on its own.
  - an "fsm step <node>" step-body command (new CMD_FSM_STEP), sugar for
    `pg_autoctl manual fsm step --pgdata /var/lib/postgres/pgaf` run
    inside the named node's container, modeled directly on the existing
    "stop postgres <node>"/"start postgres <node>" sugar. Retries for up
    to 30s on transient failure (test_runner.c): right after a step-mode
    service (re)starts there's a brief handoff window with its sibling
    postgres-controller subprocess during which a step can transiently
    fail (e.g. postmaster.pid not observed yet) -- autopilot mode
    self-heals this silently on the next tick, step mode has no next
    tick unless retried.

While building the first live spec against this, found and fixed a real
gap in step mode itself: keeper_fsm_step() never refreshes the keeper's
cached list of other nodes, unlike the autonomous tick body
(service_keeper_node_active()). This went unnoticed because the only
other caller, the one-shot "pg_autoctl manual fsm step" CLI, gets a
fresh cache every invocation via its own fresh keeper_init() -- but
step mode's node-active service calls keeper_init() exactly once and
runs for its whole lifetime, so without an explicit refresh a peer that
registered after the service started would silently never get its
replication slot or HBA rule maintained. Fixed by refreshing the cache
before every step in service_keeper.c's step-mode branch, non-fatally
(falls through to the step regardless of refresh failure, matching how
a failed refresh in autonomous mode just retries next tick).
@dimitri dimitri self-assigned this Aug 2, 2026
@dimitri dimitri added the Developer productivity Enhancements to ability to ship quality code label Aug 2, 2026
@dimitri
dimitri force-pushed the pgaftest-step-mode-and-report-advance branch from 5aa9ff9 to 6446d3c Compare August 2, 2026 03:25
dimitri added 2 commits August 2, 2026 05:28
pg_autoctl manual fsm step reports the node's current state to the
monitor and, in the same call, immediately attempts whatever transition
the monitor assigns back -- atomically, with no way to observe (or hold
a node frozen at) the moment in between.

Add manual fsm step report / manual fsm step advance as the two
independently-issuable halves of that combined call:
- report calls monitor_node_active() and persists the assigned goal
  state without attempting the transition.
- advance attempts the transition already on file (via
  keeper_fsm_reach_assigned_state) without a monitor round trip.

Plain "step" keeps its original combined behavior unchanged.

When a node's node-active service is running under PG_AUTOCTL_STEP_MODE
(step mode), it owns the keeper's FSM instead of a one-shot CLI
invocation, so step/report/advance are dispatched over a small
Unix-domain-socket protocol (step_socket.c/h) to the running service
rather than run standalone.
…t, fsm step

Add fsm_step_report_advance.pgaf: a live spec proving the report/advance
split works end to end using a no-autopilot node, registered in the
default schedule and the node.sch CI schedule.

Document the three new pieces of tooling this branch introduces:
- the no-autopilot cluster node modifier
- the pgaftest "fsm step <node>" DSL command
- the "pg_autoctl manual fsm step [report|advance]" CLI split

in docs/ref/pgaftest.rst (new "Step mode: no-autopilot nodes" section,
node modifiers table, DSL command reference, test catalog entry) and
docs/ref/pg_autoctl_manual.rst (fsm step synopsis).
@dimitri
dimitri force-pushed the pgaftest-step-mode-and-report-advance branch from 6446d3c to 7c34bc7 Compare August 2, 2026 03:30
dimitri added 8 commits August 2, 2026 15:47
The report/advance split had been documented as a paragraph spliced
into the middle of pg_autoctl_manual.rst's literal command-tree
output, breaking it into two disconnected fragments. Move that
explanation into its own page, pg_autoctl_manual_fsm_step.rst,
following the pattern already used for
pg_autoctl_manual_service_restart.rst (Synopsis/Description/Options/
Examples), and link it from the manual page's toctree instead.

Update pgaftest.rst's own cross-reference to point at the new page.
PGDATA is already set in every node container's environment
(compose_gen.c), the same way every other "exec <node> pg_autoctl ..."
command in the test suite already relies on it implicitly. Passing
--pgdata /var/lib/postgres/pgaf explicitly added nothing and was
inconsistent with the rest of the codebase; drop it from the
fsm_step_report_advance.pgaf spec and from the "fsm step <node>" DSL
command's own generated invocation (test_runner.c, plus the matching
doc comment in test_spec_parse.y).
keeper_node_active_loop() had grown a stepMode boolean threaded through
its setup, its sleep/wait dispatch, and a large "if (stepMode) { ...;
continue; }" block sitting in the middle of the ordinary autopilot
body -- two genuinely different loops interleaved in one function via
a flag checked in three separate places.

Extract the step-mode behavior into its own keeper_step_mode_loop(),
called instead of keeper_node_active_loop() at the one call site
(cli_do_service.c) when PG_AUTOCTL_STEP_MODE is set. The "node
previously dropped from the monitor" check both loops need on startup
is factored into a shared keeper_exit_if_previously_dropped() helper.
keeper_node_active_loop() itself is otherwise unchanged: same
autopilot behavior, just without the step-mode branching that never
applied to it in practice (a step-mode node never reaches any of that
code -- it always hits the step-mode block and continues).

No behavior change: verified live against fsm_step_report_advance.pgaf
(step mode) and basic_operation.pgaf (28 tests, ordinary autopilot
path), both passing.
…ing)

Renames the pgaftest cluster node modifier from "no-autopilot" to
"suspended" everywhere: lexer token, grammar rule, AST field
(TestNode.suspended), pretty-printer, and the two live specs. The
backing environment variable PG_AUTOCTL_STEP_MODE becomes
PG_AUTOCTL_SUSPENDED, and the pg_autoctl-side function that runs a
suspended node's main loop is renamed keeper_suspended_loop
(previously keeper_step_mode_loop). Comments and log messages
throughout service_keeper.c, cli_do_fsm.c, cli_do_service.c, and
step_socket.c are updated to say "suspended"/"suspended-node" instead
of "step mode"/"step-mode", since that's the same concept under a
clearer name -- inspired by the state debuggers (LLDB, Visual Studio,
Unix job control) report for a process halted at a breakpoint, rather
than the double-negative "no-autopilot".

Documentation (pgaftest.rst's node-modifier table and "Suspended
nodes" section, pg_autoctl_manual_fsm_step.rst) and the
fsm_step_report_advance.pgaf spec are updated to match. Bison/flex
outputs (test_spec_parse.c/h, test_spec_scan.c) regenerated from
their .y/.l sources.

No behavior change: verified live against fsm_step_report_advance.pgaf
(4/4 passing) and a pretty-printer round-trip (pgaftest show spec)
confirming the new keyword parses and re-emits correctly.
Verified the whole literal command-tree block against the actual
built binary's --help output for every level under "pg_autoctl
manual". Two of the mismatches were pre-existing (predate this
branch, unrelated to the fsm step / suspended work):

- "manual service pgctl" description said "...postgres controller",
  the real registered description says "...postgres service".
- "manual primary adduser" capitalized "Add" where the real output
  is lowercase "add" for both monitor/replica lines.
- "manual coordinator" and "manual coordinator update" descriptions
  were entirely reworded paraphrases; replaced with the actual
  registered CommandLine descriptions (which reference the real
  "master_update_node" Citus function by name).

The "manual fsm"/"manual fsm nodes"/"manual service"/"manual service
restart"/"manual"/"manual monitor"/"manual primary"/"manual primary
slot"/"manual standby" sections were already verified accurate and
left unchanged.
…-help

"step" is both a terminal command (bare invocation runs the combined
report+transition behavior) and effectively a parent for its "report"/
"advance" positional argument, but was registered as a plain
make_command() leaf, so it never showed the "+" marker in "pg_autoctl
manual fsm"'s own listing, and "pg_autoctl manual fsm step --help"
never expanded into a sub-command list the way "nodes" does for
"get"/"set".

Fix: give the CommandLine node (built from a raw struct literal since
the make_command()/make_command_set() macros are mutually exclusive
between "run" and "subcommands") a subcommands array listing "report"
and "advance". commandline_run() checks command->run before ever
consulting subcommands, so real dispatch is completely unaffected --
this only feeds the two existing help-printing code paths
(commandline_pretty_print_subcommands's "+" check, and
commandline_print_usage's subcommand expansion). The report/advance
entries' own "run" still points at cli_do_fsm_step for defensive
consistency, though normal dispatch never reaches them.

Verified: "pg_autoctl manual fsm" now shows "+ step", "pg_autoctl
manual fsm step --help" now lists report/advance, and the live
fsm_step_report_advance.pgaf spec still passes 4/4 (real report/
advance dispatch unchanged). docs/ref/pg_autoctl_manual_fsm_step.rst's
Synopsis updated to match the new --help output.
Follow-up to the cli_do_fsm.c change that gives "step" a (decorative)
subcommands array so the CLI's own --help output marks it with "+"
and expands report/advance -- the literal command-tree block in this
page still showed the old, unmarked "step" line.
pg_autoctl_manual.rst's convention expands every "+"-marked entry
into its own "pg_autoctl manual X Y" block further down the same
page (e.g. "fsm nodes" gets one for get/set) -- the previous commit
added the "+" marker on "step" but not this expansion block, so the
page promised a further breakdown it didn't actually show. Add it,
matching the real --help output verbatim.
@dimitri
dimitri merged commit 21f6dd4 into main Aug 2, 2026
145 of 146 checks passed
@dimitri
dimitri deleted the pgaftest-step-mode-and-report-advance branch August 2, 2026 15:24
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant