diff --git a/docs/conf.py b/docs/conf.py
index 3db4ede21..9c678d65e 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -49,6 +49,10 @@ def __init__(self, **options):
# ones.
extensions = ["sphinx.ext.githubpages", "sphinxcontrib.mermaid"]
+# Let readers pan/drag and scroll-to-zoom on rendered Mermaid diagrams --
+# useful for the FSM diagrams, several of which have 15-30 edges.
+mermaid_d3_zoom = True
+
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
diff --git a/docs/failover-state-machine.rst b/docs/failover-state-machine.rst
index acd2f3941..a0bd2d604 100644
--- a/docs/failover-state-machine.rst
+++ b/docs/failover-state-machine.rst
@@ -301,16 +301,292 @@ command, and then the node entry is removed from the monitor.
pg_auto_failover keeper's State Machine
---------------------------------------
-It is possible to use the following command to get a visual representation
-of the Keeper's Finite State Machine::
-
- $ pg_autoctl inspect fsm gv | dot -Tsvg > fsm.svg
+The full keeper FSM is 20 states and 77 transitions -- legible as a reference
+table, but too dense to read at a glance as a single diagram. ``pg_autoctl
+inspect fsm mermaid`` renders it instead as five smaller diagrams, one per
+phase of a node's life, generated directly from ``KeeperFSM[]``
+(``src/bin/pg_autoctl/fsm.c``) so they can never drift out of sync with the
+actual state machine the way a hand-maintained image can. The 68 edges shown
+below exclude ``join_primary``, a deprecated state (see `Join_primary`_
+above) no longer assigned to any node -- ``KeeperFSM[]`` still carries its 9
+transitions for backward compatibility with on-disk state from old
+versions, but they would only add clutter here::
+
+ $ pg_autoctl inspect fsm mermaid init
+ $ pg_autoctl inspect fsm mermaid steady-state
+ $ pg_autoctl inspect fsm mermaid failover
+ $ pg_autoctl inspect fsm mermaid maintenance
+ $ pg_autoctl inspect fsm mermaid removal
+
+Each command prints a `Mermaid `_ ``stateDiagram-v2``
+program; the five below are that exact output, embedded directly (via
+``sphinxcontrib.mermaid``, already a docs dependency) rather than a
+checked-in image, so this page renders from source that lives in version
+control and rebuilds automatically whenever the FSM changes.
+
+Colours are consistent across all five diagrams and group states by role,
+not by phase: **grey** states are administrative/lifecycle (``init``,
+``single``, ``dropped``), **blue** states are primary-like (taking writes,
+or about to), **green** states are secondary-like (steady replicas),
+**red** states are a primary on its way out (draining/demoted), **purple**
+states are maintenance, and **amber** states belong to the multi-standby
+candidate-election machinery.
+
+Several states legitimately appear in more than one diagram -- that is
+expected, not a partition bug, since these are narrative slices of one
+underlying graph, not a strict split. Every diagram below annotates each of
+its states with a note naming every *other* diagram that state also
+appears in, so this is never ambiguous while reading.
+
+Node init / join
+^^^^^^^^^^^^^^^^
-The `dot` program is part of the Graphviz suite and produces the following
-output:
+How a node comes into existence, or rejoins after being dropped or
+restarted.
+
+.. mermaid::
+
+ stateDiagram-v2
+ init --> single : Start as a single node
+ dropped --> single : Start as a single node
+ dropped --> report_lsn : This node is being reinitialized after having been dropped
+ single --> wait_primary : A new secondary was added
+ wait_standby --> catchingup : The primary is now ready to accept a standby
+ init --> wait_standby : Start following a primary
+ dropped --> wait_standby : Start following a primary
+ init --> report_lsn : Creating a new node from a standby node that is not a candidate.
+
+ note right of single : also appears in Node removal / drop
+ note right of dropped : also appears in Node removal / drop
+ note right of report_lsn : also appears in Failover / promotion, Maintenance, Node removal / drop
+ note right of wait_primary : also appears in Steady-state / config changes, Failover / promotion, Node removal / drop
+ note right of wait_standby : also appears in Steady-state / config changes
+ note right of catchingup : also appears in Steady-state / config changes, Failover / promotion, Maintenance, Node removal / drop
+
+ classDef metaState fill:#e0e0e0,stroke:#888888,color:#333333
+ classDef primaryState fill:#cfe2ff,stroke:#3b6fb6,color:#1a1a1a
+ classDef secondaryState fill:#d4edda,stroke:#4c9a5b,color:#1a1a1a
+ classDef electionState fill:#fff3cd,stroke:#c99a1e,color:#1a1a1a
+ class init metaState
+ class single metaState
+ class dropped metaState
+ class report_lsn electionState
+ class wait_primary primaryState
+ class wait_standby secondaryState
+ class catchingup secondaryState
+
+Steady-state / config changes
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Normal operation: no failure, no maintenance -- a secondary joining or
+leaving quorum, a settings change, a brief health blip and recovery.
+
+.. mermaid::
+
+ stateDiagram-v2
+ primary --> wait_primary : Secondary became unhealthy
+ wait_primary --> primary : A healthy secondary appeared
+ secondary --> catchingup : Failed to report back to the monitor, not eligible for promotion
+ catchingup --> secondary : Convinced the monitor that I'm up and running, and eligible for promotion again
+ secondary --> wait_standby : Registering to a new monitor
+ primary --> apply_settings : Apply new pg_auto_failover settings (synchronous_standby_names)
+ wait_primary --> apply_settings : Apply new pg_auto_failover settings (synchronous_standby_names)
+ apply_settings --> primary : Back to primary state after having applied new pg_auto_failover settings
+ apply_settings --> wait_primary : Secondary became unhealthy
+
+ note right of primary : also appears in Failover / promotion, Maintenance, Node removal / drop
+ note right of wait_primary : also appears in Node init / join, Failover / promotion, Node removal / drop
+ note right of secondary : also appears in Failover / promotion, Maintenance, Node removal / drop
+ note right of catchingup : also appears in Node init / join, Failover / promotion, Maintenance, Node removal / drop
+ note right of wait_standby : also appears in Node init / join
+ note right of apply_settings : also appears in Failover / promotion, Node removal / drop
+
+ classDef primaryState fill:#cfe2ff,stroke:#3b6fb6,color:#1a1a1a
+ classDef secondaryState fill:#d4edda,stroke:#4c9a5b,color:#1a1a1a
+ class primary primaryState
+ class wait_primary primaryState
+ class secondary secondaryState
+ class catchingup secondaryState
+ class wait_standby secondaryState
+ class apply_settings primaryState
+
+Failover / promotion
+^^^^^^^^^^^^^^^^^^^^^
+
+The primary going away and a candidate taking over, including the
+multi-standby candidate-election machinery (``report_lsn``,
+``fast_forward``, ``join_secondary``) -- this is the largest of the five,
+still less than half the size of the full graph. Citus coordinator/worker
+transitions are not shown separately: every Citus-specific transition in
+``KeeperFSM[]`` reuses an edge that already exists here, just with a
+different underlying implementation, so a separate "Citus diagram" would
+be identical in shape to this one.
+
+.. mermaid::
+
+ stateDiagram-v2
+ primary --> draining : A failover occurred, stopping writes
+ draining --> demoted : Demoted after a failover, no longer primary
+ primary --> demoted : A failover occurred, no longer primary
+ primary --> demote_timeout : A failover occurred, no longer primary
+ apply_settings --> draining : A failover occurred, stopping writes
+ apply_settings --> demoted : A failover occurred, no longer primary
+ apply_settings --> demote_timeout : A failover occurred, no longer primary
+ draining --> demote_timeout : Secondary confirms it is receiving no more writes
+ demote_timeout --> demoted : Demote timeout expired
+ wait_primary --> demoted : A failover occurred, no longer primary
+ demote_timeout --> primary : Detected a network partition, but monitor didn't do failover
+ demoted --> catchingup : A new primary is available. First, try to rewind. If that fails, do a pg_basebackup.
+ secondary --> prepare_promotion : Stop traffic to primary, wait for it to finish draining.
+ catchingup --> prepare_promotion : Stop traffic to primary, wait for it to finish draining.
+ prepare_promotion --> stop_replication : Prevent against split-brain situations.
+ stop_replication --> wait_primary : Confirmed promotion with the monitor
+ prepare_promotion --> wait_primary : Promoting a Citus Worker standby after having blocked writes from the coordinator.
+ secondary --> report_lsn : Reporting the last write-ahead log location received
+ catchingup --> report_lsn : Reporting the last write-ahead log location received
+ report_lsn --> prepare_promotion : Stop traffic to primary, wait for it to finish draining.
+ report_lsn --> fast_forward : Fetching missing WAL bits from another standby before promotion
+ fast_forward --> prepare_promotion : Got the missing WAL bytes, promoted
+ report_lsn --> join_secondary : A failover candidate has been selected, stop replication
+ report_lsn --> secondary : A failover candidate has been selected, stop replication
+ join_secondary --> secondary : Failover is done, we have a new primary to follow
+ draining --> report_lsn : Reporting the last write-ahead log location after draining
+ demoted --> report_lsn : Reporting the last write-ahead log location after being demoted
+
+ note right of primary : also appears in Steady-state / config changes, Maintenance, Node removal / drop
+ note right of draining : also appears in Node removal / drop
+ note right of demoted : also appears in Node removal / drop
+ note right of demote_timeout : also appears in Node removal / drop
+ note right of apply_settings : also appears in Steady-state / config changes, Node removal / drop
+ note right of wait_primary : also appears in Node init / join, Steady-state / config changes, Node removal / drop
+ note right of catchingup : also appears in Node init / join, Steady-state / config changes, Maintenance, Node removal / drop
+ note right of secondary : also appears in Steady-state / config changes, Maintenance, Node removal / drop
+ note right of prepare_promotion : also appears in Node removal / drop
+ note right of stop_replication : also appears in Node removal / drop
+ note right of report_lsn : also appears in Node init / join, Maintenance, Node removal / drop
+
+ classDef primaryState fill:#cfe2ff,stroke:#3b6fb6,color:#1a1a1a
+ classDef secondaryState fill:#d4edda,stroke:#4c9a5b,color:#1a1a1a
+ classDef demotingState fill:#f8d7da,stroke:#c0392b,color:#1a1a1a
+ classDef electionState fill:#fff3cd,stroke:#c99a1e,color:#1a1a1a
+ class primary primaryState
+ class draining demotingState
+ class demoted demotingState
+ class demote_timeout demotingState
+ class apply_settings primaryState
+ class wait_primary primaryState
+ class catchingup secondaryState
+ class secondary secondaryState
+ class prepare_promotion electionState
+ class stop_replication electionState
+ class report_lsn electionState
+ class fast_forward electionState
+ class join_secondary electionState
-.. figure:: ./fsm.png
- :scale: 35%
- :alt: Keeper state machine
+Maintenance
+^^^^^^^^^^^
- Keeper State Machine
+Planned maintenance on either a secondary or the primary.
+
+.. mermaid::
+
+ stateDiagram-v2
+ primary --> prepare_maintenance : Promoting the standby to enable maintenance on the primary, stopping Postgres
+ prepare_maintenance --> maintenance : Setting up Postgres in standby mode for maintenance operations
+ primary --> maintenance : Setting up Postgres in standby mode for maintenance operations
+ secondary --> wait_maintenance : Waiting for the primary to disable sync replication before going to maintenance.
+ catchingup --> wait_maintenance : Waiting for the primary to disable sync replication before going to maintenance.
+ secondary --> maintenance : Suspending standby for manual maintenance.
+ catchingup --> maintenance : Suspending standby for manual maintenance.
+ wait_maintenance --> maintenance : Suspending standby for manual maintenance.
+ maintenance --> catchingup : Restarting standby after manual maintenance is done.
+ prepare_maintenance --> catchingup : Restarting standby after manual maintenance is done.
+ maintenance --> report_lsn : Reporting the last write-ahead log location received
+ prepare_maintenance --> report_lsn : Reporting the last write-ahead log location received
+
+ note right of primary : also appears in Steady-state / config changes, Failover / promotion, Node removal / drop
+ note right of secondary : also appears in Steady-state / config changes, Failover / promotion, Node removal / drop
+ note right of catchingup : also appears in Node init / join, Steady-state / config changes, Failover / promotion, Node removal / drop
+ note right of report_lsn : also appears in Node init / join, Failover / promotion, Node removal / drop
+
+ classDef primaryState fill:#cfe2ff,stroke:#3b6fb6,color:#1a1a1a
+ classDef secondaryState fill:#d4edda,stroke:#4c9a5b,color:#1a1a1a
+ classDef maintenanceState fill:#e8dff5,stroke:#8e6bb0,color:#1a1a1a
+ classDef electionState fill:#fff3cd,stroke:#c99a1e,color:#1a1a1a
+ class primary primaryState
+ class prepare_maintenance maintenanceState
+ class maintenance maintenanceState
+ class secondary secondaryState
+ class wait_maintenance maintenanceState
+ class catchingup secondaryState
+ class report_lsn electionState
+
+Node removal / drop
+^^^^^^^^^^^^^^^^^^^^
+
+An operator forcibly removing a node (``pg_autoctl drop node``), or a peer
+node reacting to the other side of that removal.
+
+.. mermaid::
+
+ stateDiagram-v2
+ primary --> single : Other node was forcibly removed, now single
+ wait_primary --> single : Other node was forcibly removed, now single
+ demoted --> single : Was demoted after a failure, but secondary was forcibly removed
+ demote_timeout --> single : Was demoted after a failure, but secondary was forcibly removed
+ draining --> single : Was demoted after a failure, but secondary was forcibly removed
+ secondary --> single : Primary was forcibly removed
+ catchingup --> single : Primary was forcibly removed
+ prepare_promotion --> single : Primary was forcibly removed
+ stop_replication --> single : Went down to force the primary to time out, but then it was removed
+ report_lsn --> single : There is no other node anymore, promote this node
+ apply_settings --> single : Other node was forcibly removed, now single
+ any_state --> dropped : This node is being dropped from the monitor
+
+ note right of primary : also appears in Steady-state / config changes, Failover / promotion, Maintenance
+ note right of single : also appears in Node init / join
+ note right of wait_primary : also appears in Node init / join, Steady-state / config changes, Failover / promotion
+ note right of demoted : also appears in Failover / promotion
+ note right of demote_timeout : also appears in Failover / promotion
+ note right of draining : also appears in Failover / promotion
+ note right of secondary : also appears in Steady-state / config changes, Failover / promotion, Maintenance
+ note right of catchingup : also appears in Node init / join, Steady-state / config changes, Failover / promotion, Maintenance
+ note right of prepare_promotion : also appears in Failover / promotion
+ note right of stop_replication : also appears in Failover / promotion
+ note right of report_lsn : also appears in Node init / join, Failover / promotion, Maintenance
+ note right of apply_settings : also appears in Steady-state / config changes, Failover / promotion
+ note right of dropped : also appears in Node init / join
+
+ classDef metaState fill:#e0e0e0,stroke:#888888,color:#333333
+ classDef primaryState fill:#cfe2ff,stroke:#3b6fb6,color:#1a1a1a
+ classDef secondaryState fill:#d4edda,stroke:#4c9a5b,color:#1a1a1a
+ classDef demotingState fill:#f8d7da,stroke:#c0392b,color:#1a1a1a
+ classDef electionState fill:#fff3cd,stroke:#c99a1e,color:#1a1a1a
+ class primary primaryState
+ class single metaState
+ class wait_primary primaryState
+ class demoted demotingState
+ class demote_timeout demotingState
+ class draining demotingState
+ class secondary secondaryState
+ class catchingup secondaryState
+ class prepare_promotion electionState
+ class stop_replication electionState
+ class report_lsn electionState
+ class apply_settings primaryState
+ class any_state metaState
+ class dropped metaState
+
+.. note::
+
+ This replaces the previous single Graphviz diagram (``pg_autoctl inspect
+ fsm gv | dot -Tsvg``, rendered from a checked-in ``fsm.png`` last
+ regenerated by hand in 2021). The five diagrams above cover every
+ currently-reachable transition the old single diagram did -- 68 edges,
+ split by phase rather than shown at once -- deliberately excluding only
+ the 9 transitions involving the deprecated ``join_primary`` state, so
+ ``fsm.png`` is no longer needed as documentation. The ``pg_autoctl
+ inspect fsm gv`` command itself is left in place for anyone who wants
+ the full graph, ``join_primary`` included, as one Graphviz file (e.g.
+ to pipe into their own
+ tooling), but it is no longer the documented way to visualize the FSM.
diff --git a/src/bin/pg_autoctl/cli_do_fsm.c b/src/bin/pg_autoctl/cli_do_fsm.c
index 02fbfa337..8132cedac 100644
--- a/src/bin/pg_autoctl/cli_do_fsm.c
+++ b/src/bin/pg_autoctl/cli_do_fsm.c
@@ -19,6 +19,7 @@
#include "commandline.h"
#include "defaults.h"
#include "fsm.h"
+#include "fsm_mermaid.h"
#include "keeper_config.h"
#include "keeper.h"
#include "parsing.h"
@@ -31,6 +32,11 @@ static void cli_do_fsm_init(int argc, char **argv);
static void cli_do_fsm_state(int argc, char **argv);
static void cli_do_fsm_list(int argc, char **argv);
static void cli_do_fsm_gv(int argc, char **argv);
+static void cli_do_fsm_mermaid_init(int argc, char **argv);
+static void cli_do_fsm_mermaid_steady_state(int argc, char **argv);
+static void cli_do_fsm_mermaid_failover(int argc, char **argv);
+static void cli_do_fsm_mermaid_maintenance(int argc, char **argv);
+static void cli_do_fsm_mermaid_removal(int argc, char **argv);
static void cli_do_fsm_assign(int argc, char **argv);
static void cli_do_fsm_step(int argc, char **argv);
@@ -66,6 +72,47 @@ CommandLine fsm_gv =
"Output the FSM as a .gv program suitable for graphviz/dot",
"", NULL, NULL, cli_do_fsm_gv);
+static CommandLine fsm_mermaid_init =
+ make_command("init",
+ "Mermaid diagram: how a node comes into existence or rejoins",
+ "", NULL, NULL, cli_do_fsm_mermaid_init);
+
+static CommandLine fsm_mermaid_steady_state =
+ make_command("steady-state",
+ "Mermaid diagram: normal operation, no failure",
+ "", NULL, NULL, cli_do_fsm_mermaid_steady_state);
+
+static CommandLine fsm_mermaid_failover =
+ make_command("failover",
+ "Mermaid diagram: primary failover/promotion, including "
+ "multi-standby candidate election",
+ "", NULL, NULL, cli_do_fsm_mermaid_failover);
+
+static CommandLine fsm_mermaid_maintenance =
+ make_command("maintenance",
+ "Mermaid diagram: planned maintenance",
+ "", NULL, NULL, cli_do_fsm_mermaid_maintenance);
+
+static CommandLine fsm_mermaid_removal =
+ make_command("removal",
+ "Mermaid diagram: node removal/drop",
+ "", NULL, NULL, cli_do_fsm_mermaid_removal);
+
+static CommandLine *fsm_mermaid_[] = {
+ &fsm_mermaid_init,
+ &fsm_mermaid_steady_state,
+ &fsm_mermaid_failover,
+ &fsm_mermaid_maintenance,
+ &fsm_mermaid_removal,
+ NULL
+};
+
+CommandLine fsm_mermaid =
+ make_command_set("mermaid",
+ "Output the FSM as Mermaid stateDiagram-v2 programs, "
+ "split by phase for readability", NULL, NULL,
+ NULL, fsm_mermaid_);
+
CommandLine fsm_assign =
make_command("assign",
"Assign a new goal state to the keeper",
@@ -115,6 +162,7 @@ static CommandLine *fsm[] = {
&fsm_state,
&fsm_list,
&fsm_gv,
+ &fsm_mermaid,
&fsm_assign,
&fsm_step,
&fsm_nodes,
@@ -277,6 +325,46 @@ cli_do_fsm_gv(int argc, char **argv)
}
+/*
+ * cli_do_fsm_mermaid_{init,steady_state,failover,maintenance,removal} each
+ * output one phase of the FSM as a Mermaid stateDiagram-v2 program. See
+ * fsm_mermaid.c for why the phases are split this way.
+ */
+static void
+cli_do_fsm_mermaid_init(int argc, char **argv)
+{
+ print_fsm_mermaid_for_phase(FSM_PHASE_INIT);
+}
+
+
+static void
+cli_do_fsm_mermaid_steady_state(int argc, char **argv)
+{
+ print_fsm_mermaid_for_phase(FSM_PHASE_STEADY_STATE);
+}
+
+
+static void
+cli_do_fsm_mermaid_failover(int argc, char **argv)
+{
+ print_fsm_mermaid_for_phase(FSM_PHASE_FAILOVER);
+}
+
+
+static void
+cli_do_fsm_mermaid_maintenance(int argc, char **argv)
+{
+ print_fsm_mermaid_for_phase(FSM_PHASE_MAINTENANCE);
+}
+
+
+static void
+cli_do_fsm_mermaid_removal(int argc, char **argv)
+{
+ print_fsm_mermaid_for_phase(FSM_PHASE_REMOVAL);
+}
+
+
/*
* cli_do_fsm_assigns a reachable state from the current one.
*/
diff --git a/src/bin/pg_autoctl/cli_do_root.h b/src/bin/pg_autoctl/cli_do_root.h
index b0fc6f50b..0706b95ef 100644
--- a/src/bin/pg_autoctl/cli_do_root.h
+++ b/src/bin/pg_autoctl/cli_do_root.h
@@ -21,6 +21,7 @@ extern CommandLine fsm_state;
extern CommandLine fsm_node_state;
extern CommandLine fsm_list;
extern CommandLine fsm_gv;
+extern CommandLine fsm_mermaid;
/* mutating sub-commands exposed via "pg_autoctl manual fsm" */
extern CommandLine fsm_init;
diff --git a/src/bin/pg_autoctl/cli_inspect.c b/src/bin/pg_autoctl/cli_inspect.c
index 968c471f2..3bb441b80 100644
--- a/src/bin/pg_autoctl/cli_inspect.c
+++ b/src/bin/pg_autoctl/cli_inspect.c
@@ -25,6 +25,7 @@ static CommandLine *inspect_fsm_subcommands[] = {
&fsm_state,
&fsm_list,
&fsm_gv,
+ &fsm_mermaid,
NULL
};
diff --git a/src/bin/pg_autoctl/fsm.c b/src/bin/pg_autoctl/fsm.c
index 3c89a32d8..60797958e 100644
--- a/src/bin/pg_autoctl/fsm.c
+++ b/src/bin/pg_autoctl/fsm.c
@@ -216,7 +216,8 @@ KeeperFSMTransition KeeperFSM[] = {
{
INIT_STATE, SINGLE_STATE, NODE_KIND_ANY,
COMMENT_INIT_TO_SINGLE,
- &fsm_init_primary
+ &fsm_init_primary,
+ FSM_PHASE_INIT
},
{
@@ -228,13 +229,15 @@ KeeperFSMTransition KeeperFSM[] = {
{
DROPPED_STATE, SINGLE_STATE, NODE_KIND_ANY,
COMMENT_INIT_TO_SINGLE,
- &fsm_init_primary
+ &fsm_init_primary,
+ FSM_PHASE_INIT
},
{
DROPPED_STATE, REPORT_LSN_STATE, NODE_KIND_ANY,
COMMENT_DROPPED_TO_REPORT_LSN,
- &fsm_init_from_standby
+ &fsm_init_from_standby,
+ FSM_PHASE_INIT
},
/*
@@ -243,19 +246,22 @@ KeeperFSMTransition KeeperFSM[] = {
{
PRIMARY_STATE, SINGLE_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_SINGLE,
- &fsm_disable_replication
+ &fsm_disable_replication,
+ FSM_PHASE_REMOVAL
},
{
WAIT_PRIMARY_STATE, SINGLE_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_SINGLE,
- &fsm_disable_replication
+ &fsm_disable_replication,
+ FSM_PHASE_REMOVAL
},
{
JOIN_PRIMARY_STATE, SINGLE_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_SINGLE,
- &fsm_disable_replication
+ &fsm_disable_replication,
+ FSM_PHASE_REMOVAL
},
/*
@@ -264,61 +270,71 @@ KeeperFSMTransition KeeperFSM[] = {
{
PRIMARY_STATE, DRAINING_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_DRAINING,
- &fsm_stop_postgres
+ &fsm_stop_postgres,
+ FSM_PHASE_FAILOVER
},
{
DRAINING_STATE, DEMOTED_STATE, NODE_KIND_ANY,
COMMENT_DRAINING_TO_DEMOTED,
- &fsm_stop_postgres
+ &fsm_stop_postgres,
+ FSM_PHASE_FAILOVER
},
{
PRIMARY_STATE, DEMOTED_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_DEMOTED,
- &fsm_stop_postgres
+ &fsm_stop_postgres,
+ FSM_PHASE_FAILOVER
},
{
PRIMARY_STATE, DEMOTE_TIMEOUT_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_DEMOTED,
- &fsm_stop_postgres
+ &fsm_stop_postgres,
+ FSM_PHASE_FAILOVER
},
{
JOIN_PRIMARY_STATE, DRAINING_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_DRAINING,
- &fsm_stop_postgres
+ &fsm_stop_postgres,
+ FSM_PHASE_FAILOVER
},
{
JOIN_PRIMARY_STATE, DEMOTED_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_DEMOTED,
- &fsm_stop_postgres
+ &fsm_stop_postgres,
+ FSM_PHASE_FAILOVER
},
{
JOIN_PRIMARY_STATE, DEMOTE_TIMEOUT_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_DEMOTED,
- &fsm_stop_postgres
+ &fsm_stop_postgres,
+ FSM_PHASE_FAILOVER
},
{
APPLY_SETTINGS_STATE, DRAINING_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_DRAINING,
- &fsm_stop_postgres
+ &fsm_stop_postgres,
+ FSM_PHASE_FAILOVER
},
{
APPLY_SETTINGS_STATE, DEMOTED_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_DEMOTED,
- &fsm_stop_postgres
+ &fsm_stop_postgres,
+ FSM_PHASE_FAILOVER
},
{
APPLY_SETTINGS_STATE, DEMOTE_TIMEOUT_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_DEMOTED,
- &fsm_stop_postgres
+ &fsm_stop_postgres,
+ FSM_PHASE_FAILOVER
},
/*
@@ -327,19 +343,22 @@ KeeperFSMTransition KeeperFSM[] = {
{
PRIMARY_STATE, PREPARE_MAINTENANCE_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_PREPARE_MAINTENANCE,
- &fsm_stop_postgres_for_primary_maintenance
+ &fsm_stop_postgres_for_primary_maintenance,
+ FSM_PHASE_MAINTENANCE
},
{
PREPARE_MAINTENANCE_STATE, MAINTENANCE_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_MAINTENANCE,
- &fsm_stop_postgres_and_setup_standby
+ &fsm_stop_postgres_and_setup_standby,
+ FSM_PHASE_MAINTENANCE
},
{
PRIMARY_STATE, MAINTENANCE_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_MAINTENANCE,
- &fsm_stop_postgres_for_primary_maintenance
+ &fsm_stop_postgres_for_primary_maintenance,
+ FSM_PHASE_MAINTENANCE
},
/*
@@ -348,13 +367,15 @@ KeeperFSMTransition KeeperFSM[] = {
{
DRAINING_STATE, DEMOTE_TIMEOUT_STATE, NODE_KIND_ANY,
COMMENT_DRAINING_TO_DEMOTE_TIMEOUT,
- &fsm_stop_postgres
+ &fsm_stop_postgres,
+ FSM_PHASE_FAILOVER
},
{
DEMOTE_TIMEOUT_STATE, DEMOTED_STATE, NODE_KIND_ANY,
COMMENT_DEMOTE_TIMEOUT_TO_DEMOTED,
- &fsm_stop_postgres
+ &fsm_stop_postgres,
+ FSM_PHASE_FAILOVER
},
/*
@@ -363,7 +384,8 @@ KeeperFSMTransition KeeperFSM[] = {
{
WAIT_PRIMARY_STATE, DEMOTED_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_DEMOTED,
- &fsm_stop_postgres
+ &fsm_stop_postgres,
+ FSM_PHASE_FAILOVER
},
/*
@@ -378,7 +400,8 @@ KeeperFSMTransition KeeperFSM[] = {
{
DEMOTED_STATE, SINGLE_STATE, NODE_KIND_ANY,
COMMENT_DEMOTED_TO_SINGLE,
- &fsm_resume_as_primary
+ &fsm_resume_as_primary,
+ FSM_PHASE_REMOVAL
},
{
@@ -390,7 +413,8 @@ KeeperFSMTransition KeeperFSM[] = {
{
DEMOTE_TIMEOUT_STATE, SINGLE_STATE, NODE_KIND_ANY,
COMMENT_DEMOTED_TO_SINGLE,
- &fsm_resume_as_primary
+ &fsm_resume_as_primary,
+ FSM_PHASE_REMOVAL
},
{
@@ -402,7 +426,8 @@ KeeperFSMTransition KeeperFSM[] = {
{
DRAINING_STATE, SINGLE_STATE, NODE_KIND_ANY,
COMMENT_DEMOTED_TO_SINGLE,
- &fsm_resume_as_primary
+ &fsm_resume_as_primary,
+ FSM_PHASE_REMOVAL
},
/*
@@ -423,7 +448,8 @@ KeeperFSMTransition KeeperFSM[] = {
{
SECONDARY_STATE, SINGLE_STATE, NODE_KIND_ANY,
COMMENT_LOST_PRIMARY,
- &fsm_promote_standby
+ &fsm_promote_standby,
+ FSM_PHASE_REMOVAL
},
{
@@ -441,7 +467,8 @@ KeeperFSMTransition KeeperFSM[] = {
{
CATCHINGUP_STATE, SINGLE_STATE, NODE_KIND_ANY,
COMMENT_LOST_PRIMARY,
- &fsm_promote_standby
+ &fsm_promote_standby,
+ FSM_PHASE_REMOVAL
},
{
@@ -459,7 +486,8 @@ KeeperFSMTransition KeeperFSM[] = {
{
PREP_PROMOTION_STATE, SINGLE_STATE, NODE_KIND_ANY,
COMMENT_LOST_PRIMARY,
- &fsm_promote_standby
+ &fsm_promote_standby,
+ FSM_PHASE_REMOVAL
},
/*
@@ -474,7 +502,8 @@ KeeperFSMTransition KeeperFSM[] = {
{
STOP_REPLICATION_STATE, SINGLE_STATE, NODE_KIND_ANY,
COMMENT_REPLICATION_TO_SINGLE,
- &fsm_promote_standby
+ &fsm_promote_standby,
+ FSM_PHASE_REMOVAL
},
/*
@@ -483,7 +512,8 @@ KeeperFSMTransition KeeperFSM[] = {
{
REPORT_LSN_STATE, SINGLE_STATE, NODE_KIND_ANY,
COMMENT_REPORT_LSN_TO_SINGLE,
- &fsm_promote_standby
+ &fsm_promote_standby,
+ FSM_PHASE_REMOVAL
},
@@ -493,31 +523,36 @@ KeeperFSMTransition KeeperFSM[] = {
{
SINGLE_STATE, WAIT_PRIMARY_STATE, NODE_KIND_ANY,
COMMENT_SINGLE_TO_WAIT_PRIMARY,
- &fsm_prepare_replication
+ &fsm_prepare_replication,
+ FSM_PHASE_INIT
},
{
PRIMARY_STATE, JOIN_PRIMARY_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_JOIN_PRIMARY,
- &fsm_prepare_replication
+ &fsm_prepare_replication,
+ FSM_PHASE_STEADY_STATE
},
{
PRIMARY_STATE, WAIT_PRIMARY_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_WAIT_PRIMARY,
- &fsm_disable_sync_rep
+ &fsm_disable_sync_rep,
+ FSM_PHASE_STEADY_STATE
},
{
JOIN_PRIMARY_STATE, WAIT_PRIMARY_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_WAIT_PRIMARY,
- &fsm_disable_sync_rep
+ &fsm_disable_sync_rep,
+ FSM_PHASE_STEADY_STATE
},
{
WAIT_PRIMARY_STATE, JOIN_PRIMARY_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_JOIN_PRIMARY,
- &fsm_prepare_replication
+ &fsm_prepare_replication,
+ FSM_PHASE_STEADY_STATE
},
/*
@@ -526,19 +561,22 @@ KeeperFSMTransition KeeperFSM[] = {
{
WAIT_PRIMARY_STATE, PRIMARY_STATE, NODE_KIND_ANY,
COMMENT_WAIT_PRIMARY_TO_PRIMARY,
- &fsm_enable_sync_rep
+ &fsm_enable_sync_rep,
+ FSM_PHASE_STEADY_STATE
},
{
JOIN_PRIMARY_STATE, PRIMARY_STATE, NODE_KIND_ANY,
COMMENT_JOIN_PRIMARY_TO_PRIMARY,
- &fsm_enable_sync_rep
+ &fsm_enable_sync_rep,
+ FSM_PHASE_STEADY_STATE
},
{
DEMOTE_TIMEOUT_STATE, PRIMARY_STATE, NODE_KIND_ANY,
COMMENT_DEMOTE_TO_PRIMARY,
- &fsm_start_postgres
+ &fsm_start_postgres,
+ FSM_PHASE_FAILOVER
},
/*
@@ -547,19 +585,22 @@ KeeperFSMTransition KeeperFSM[] = {
{
WAIT_STANDBY_STATE, CATCHINGUP_STATE, NODE_KIND_ANY,
COMMENT_WAIT_STANDBY_TO_CATCHINGUP,
- &fsm_init_standby
+ &fsm_init_standby,
+ FSM_PHASE_INIT
},
{
DEMOTED_STATE, CATCHINGUP_STATE, NODE_KIND_ANY,
COMMENT_DEMOTED_TO_CATCHINGUP,
- &fsm_rewind_or_init
+ &fsm_rewind_or_init,
+ FSM_PHASE_FAILOVER
},
{
SECONDARY_STATE, CATCHINGUP_STATE, NODE_KIND_ANY,
COMMENT_SECONDARY_TO_CATCHINGUP,
- &fsm_follow_new_primary
+ &fsm_follow_new_primary,
+ FSM_PHASE_STEADY_STATE
},
/*
@@ -574,7 +615,8 @@ KeeperFSMTransition KeeperFSM[] = {
{
CATCHINGUP_STATE, SECONDARY_STATE, NODE_KIND_ANY,
COMMENT_CATCHINGUP_TO_SECONDARY,
- &fsm_prepare_for_secondary
+ &fsm_prepare_for_secondary,
+ FSM_PHASE_STEADY_STATE
},
/*
@@ -589,7 +631,8 @@ KeeperFSMTransition KeeperFSM[] = {
{
SECONDARY_STATE, PREP_PROMOTION_STATE, NODE_KIND_ANY,
COMMENT_SECONDARY_TO_PREP_PROMOTION,
- &fsm_prepare_standby_for_promotion
+ &fsm_prepare_standby_for_promotion,
+ FSM_PHASE_FAILOVER
},
{
@@ -601,7 +644,8 @@ KeeperFSMTransition KeeperFSM[] = {
{
CATCHINGUP_STATE, PREP_PROMOTION_STATE, NODE_KIND_ANY,
COMMENT_SECONDARY_TO_PREP_PROMOTION,
- &fsm_prepare_standby_for_promotion
+ &fsm_prepare_standby_for_promotion,
+ FSM_PHASE_FAILOVER
},
/*
@@ -616,7 +660,8 @@ KeeperFSMTransition KeeperFSM[] = {
{
PREP_PROMOTION_STATE, STOP_REPLICATION_STATE, NODE_KIND_ANY,
COMMENT_PROMOTION_TO_STOP_REPLICATION,
- &fsm_stop_replication
+ &fsm_stop_replication,
+ FSM_PHASE_FAILOVER
},
/*
@@ -637,7 +682,8 @@ KeeperFSMTransition KeeperFSM[] = {
{
STOP_REPLICATION_STATE, WAIT_PRIMARY_STATE, NODE_KIND_ANY,
COMMENT_STOP_REPLICATION_TO_WAIT_PRIMARY,
- &fsm_promote_standby_to_primary
+ &fsm_promote_standby_to_primary,
+ FSM_PHASE_FAILOVER
},
{
@@ -649,7 +695,8 @@ KeeperFSMTransition KeeperFSM[] = {
{
PREP_PROMOTION_STATE, WAIT_PRIMARY_STATE, NODE_KIND_ANY,
COMMENT_BLOCKED_WRITES,
- &fsm_promote_standby
+ &fsm_promote_standby,
+ FSM_PHASE_FAILOVER
},
/*
@@ -658,13 +705,15 @@ KeeperFSMTransition KeeperFSM[] = {
{
INIT_STATE, WAIT_STANDBY_STATE, NODE_KIND_ANY,
COMMENT_INIT_TO_WAIT_STANDBY,
- NULL
+ NULL,
+ FSM_PHASE_INIT
},
{
DROPPED_STATE, WAIT_STANDBY_STATE, NODE_KIND_ANY,
COMMENT_INIT_TO_WAIT_STANDBY,
- NULL
+ NULL,
+ FSM_PHASE_INIT
},
/*
@@ -674,7 +723,8 @@ KeeperFSMTransition KeeperFSM[] = {
{
SECONDARY_STATE, WAIT_STANDBY_STATE, NODE_KIND_ANY,
COMMENT_SECONARY_TO_WAIT_STANDBY,
- NULL
+ NULL,
+ FSM_PHASE_STEADY_STATE
},
/*
@@ -683,43 +733,50 @@ KeeperFSMTransition KeeperFSM[] = {
{
SECONDARY_STATE, WAIT_MAINTENANCE_STATE, NODE_KIND_ANY,
COMMENT_SECONDARY_TO_WAIT_MAINTENANCE,
- NULL
+ NULL,
+ FSM_PHASE_MAINTENANCE
},
{
CATCHINGUP_STATE, WAIT_MAINTENANCE_STATE, NODE_KIND_ANY,
COMMENT_SECONDARY_TO_WAIT_MAINTENANCE,
- NULL
+ NULL,
+ FSM_PHASE_MAINTENANCE
},
{
SECONDARY_STATE, MAINTENANCE_STATE, NODE_KIND_ANY,
COMMENT_SECONDARY_TO_MAINTENANCE,
- &fsm_start_maintenance_on_standby
+ &fsm_start_maintenance_on_standby,
+ FSM_PHASE_MAINTENANCE
},
{
CATCHINGUP_STATE, MAINTENANCE_STATE, NODE_KIND_ANY,
COMMENT_SECONDARY_TO_MAINTENANCE,
- &fsm_start_maintenance_on_standby
+ &fsm_start_maintenance_on_standby,
+ FSM_PHASE_MAINTENANCE
},
{
WAIT_MAINTENANCE_STATE, MAINTENANCE_STATE, NODE_KIND_ANY,
COMMENT_SECONDARY_TO_MAINTENANCE,
- &fsm_start_maintenance_on_standby
+ &fsm_start_maintenance_on_standby,
+ FSM_PHASE_MAINTENANCE
},
{
MAINTENANCE_STATE, CATCHINGUP_STATE, NODE_KIND_ANY,
COMMENT_MAINTENANCE_TO_CATCHINGUP,
- &fsm_restart_standby
+ &fsm_restart_standby,
+ FSM_PHASE_MAINTENANCE
},
{
PREPARE_MAINTENANCE_STATE, CATCHINGUP_STATE, NODE_KIND_ANY,
COMMENT_MAINTENANCE_TO_CATCHINGUP,
- &fsm_restart_standby
+ &fsm_restart_standby,
+ FSM_PHASE_MAINTENANCE
},
/*
@@ -731,37 +788,43 @@ KeeperFSMTransition KeeperFSM[] = {
{
PRIMARY_STATE, APPLY_SETTINGS_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_APPLY_SETTINGS,
- NULL
+ NULL,
+ FSM_PHASE_STEADY_STATE
},
{
WAIT_PRIMARY_STATE, APPLY_SETTINGS_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_APPLY_SETTINGS,
- NULL
+ NULL,
+ FSM_PHASE_STEADY_STATE
},
{
APPLY_SETTINGS_STATE, PRIMARY_STATE, NODE_KIND_ANY,
COMMENT_APPLY_SETTINGS_TO_PRIMARY,
- &fsm_enable_sync_rep
+ &fsm_enable_sync_rep,
+ FSM_PHASE_STEADY_STATE
},
{
APPLY_SETTINGS_STATE, SINGLE_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_SINGLE,
- &fsm_disable_replication
+ &fsm_disable_replication,
+ FSM_PHASE_REMOVAL
},
{
APPLY_SETTINGS_STATE, WAIT_PRIMARY_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_WAIT_PRIMARY,
- &fsm_disable_sync_rep
+ &fsm_disable_sync_rep,
+ FSM_PHASE_STEADY_STATE
},
{
APPLY_SETTINGS_STATE, JOIN_PRIMARY_STATE, NODE_KIND_ANY,
COMMENT_PRIMARY_TO_JOIN_PRIMARY,
- &fsm_prepare_replication
+ &fsm_prepare_replication,
+ FSM_PHASE_STEADY_STATE
},
/*
@@ -771,25 +834,29 @@ KeeperFSMTransition KeeperFSM[] = {
{
SECONDARY_STATE, REPORT_LSN_STATE, NODE_KIND_ANY,
COMMENT_SECONDARY_TO_REPORT_LSN,
- &fsm_report_lsn
+ &fsm_report_lsn,
+ FSM_PHASE_FAILOVER
},
{
CATCHINGUP_STATE, REPORT_LSN_STATE, NODE_KIND_ANY,
COMMENT_SECONDARY_TO_REPORT_LSN,
- &fsm_report_lsn
+ &fsm_report_lsn,
+ FSM_PHASE_FAILOVER
},
{
MAINTENANCE_STATE, REPORT_LSN_STATE, NODE_KIND_ANY,
COMMENT_SECONDARY_TO_REPORT_LSN,
- &fsm_report_lsn
+ &fsm_report_lsn,
+ FSM_PHASE_MAINTENANCE
},
{
PREPARE_MAINTENANCE_STATE, REPORT_LSN_STATE, NODE_KIND_ANY,
COMMENT_SECONDARY_TO_REPORT_LSN,
- &fsm_report_lsn
+ &fsm_report_lsn,
+ FSM_PHASE_MAINTENANCE
},
{
@@ -801,13 +868,15 @@ KeeperFSMTransition KeeperFSM[] = {
{
REPORT_LSN_STATE, PREP_PROMOTION_STATE, NODE_KIND_ANY,
COMMENT_REPORT_LSN_TO_PREP_PROMOTION,
- &fsm_prepare_standby_for_promotion
+ &fsm_prepare_standby_for_promotion,
+ FSM_PHASE_FAILOVER
},
{
REPORT_LSN_STATE, FAST_FORWARD_STATE, NODE_KIND_ANY,
COMMENT_REPORT_LSN_TO_FAST_FORWARD,
- &fsm_fast_forward
+ &fsm_fast_forward,
+ FSM_PHASE_FAILOVER
},
{
@@ -819,25 +888,29 @@ KeeperFSMTransition KeeperFSM[] = {
{
FAST_FORWARD_STATE, PREP_PROMOTION_STATE, NODE_KIND_ANY,
COMMENT_FAST_FORWARD_TO_PREP_PROMOTION,
- &fsm_cleanup_as_primary
+ &fsm_cleanup_as_primary,
+ FSM_PHASE_FAILOVER
},
{
REPORT_LSN_STATE, JOIN_SECONDARY_STATE, NODE_KIND_ANY,
COMMENT_REPORT_LSN_TO_JOIN_SECONDARY,
- &fsm_checkpoint_and_stop_postgres
+ &fsm_checkpoint_and_stop_postgres,
+ FSM_PHASE_FAILOVER
},
{
REPORT_LSN_STATE, SECONDARY_STATE, NODE_KIND_ANY,
COMMENT_REPORT_LSN_TO_JOIN_SECONDARY,
- &fsm_follow_new_primary
+ &fsm_follow_new_primary,
+ FSM_PHASE_FAILOVER
},
{
JOIN_SECONDARY_STATE, SECONDARY_STATE, NODE_KIND_ANY,
COMMENT_JOIN_SECONDARY_TO_SECONDARY,
- &fsm_follow_new_primary
+ &fsm_follow_new_primary,
+ FSM_PHASE_FAILOVER
},
/*
@@ -847,13 +920,15 @@ KeeperFSMTransition KeeperFSM[] = {
{
DRAINING_STATE, REPORT_LSN_STATE, NODE_KIND_ANY,
COMMENT_DRAINING_TO_REPORT_LSN,
- &fsm_report_lsn_and_drop_replication_slots
+ &fsm_report_lsn_and_drop_replication_slots,
+ FSM_PHASE_FAILOVER
},
{
DEMOTED_STATE, REPORT_LSN_STATE, NODE_KIND_ANY,
COMMENT_DEMOTED_TO_REPORT_LSN,
- &fsm_report_lsn_and_drop_replication_slots
+ &fsm_report_lsn_and_drop_replication_slots,
+ FSM_PHASE_FAILOVER
},
/*
@@ -863,7 +938,8 @@ KeeperFSMTransition KeeperFSM[] = {
{
INIT_STATE, REPORT_LSN_STATE, NODE_KIND_ANY,
COMMENT_INIT_TO_REPORT_LSN,
- &fsm_init_from_standby
+ &fsm_init_from_standby,
+ FSM_PHASE_INIT
},
/*
@@ -878,7 +954,8 @@ KeeperFSMTransition KeeperFSM[] = {
{
ANY_STATE, DROPPED_STATE, NODE_KIND_ANY,
COMMENT_ANY_TO_DROPPED,
- &fsm_drop_node
+ &fsm_drop_node,
+ FSM_PHASE_REMOVAL
},
/*
diff --git a/src/bin/pg_autoctl/fsm.h b/src/bin/pg_autoctl/fsm.h
index 542635acd..2c9969f39 100644
--- a/src/bin/pg_autoctl/fsm.h
+++ b/src/bin/pg_autoctl/fsm.h
@@ -24,6 +24,28 @@
*/
typedef bool (*ReachAssignedStateFunction)(Keeper *keeper);
+/*
+ * FsmPhase groups NODE_KIND_ANY transitions into the narrative phase of a
+ * node's life they belong to -- used only to split "pg_autoctl inspect fsm
+ * mermaid" into several smaller diagrams instead of one dense graph (see
+ * fsm_mermaid.c). FSM_PHASE_NONE (the zero value) marks a row that either
+ * doesn't apply (Citus-specific rows never need a phase, since the mermaid
+ * generator skips non-NODE_KIND_ANY rows entirely) or that a NODE_KIND_ANY
+ * row was added without being classified -- fsm_mermaid.c warns on the
+ * latter rather than silently rendering nothing for it.
+ */
+typedef enum
+{
+ FSM_PHASE_NONE = 0,
+ FSM_PHASE_INIT,
+ FSM_PHASE_STEADY_STATE,
+ FSM_PHASE_FAILOVER,
+ FSM_PHASE_MAINTENANCE,
+ FSM_PHASE_REMOVAL,
+
+ FSM_PHASE_COUNT
+} FsmPhase;
+
/* defines a possible transition in the FSM */
typedef struct KeeperFSMTransition
{
@@ -32,6 +54,7 @@ typedef struct KeeperFSMTransition
PgInstanceKind pgKind;
const char *comment;
ReachAssignedStateFunction transitionFunction;
+ FsmPhase phase;
} KeeperFSMTransition;
/* src/bin/pg_autoctl/fsm.c */
diff --git a/src/bin/pg_autoctl/fsm_mermaid.c b/src/bin/pg_autoctl/fsm_mermaid.c
new file mode 100644
index 000000000..fd8f55d14
--- /dev/null
+++ b/src/bin/pg_autoctl/fsm_mermaid.c
@@ -0,0 +1,406 @@
+/*
+ * src/bin/pg_autoctl/fsm_mermaid.c
+ * Mermaid stateDiagram-v2 rendering of the keeper FSM (KeeperFSM[] in
+ * fsm.c), split into narrative phases so each diagram stays small enough
+ * to read at a glance, instead of one dense 77-edge/20-state graph.
+ *
+ * There is exactly one FSM table: KeeperFSM[] in fsm.c. Its `phase` field
+ * (declared alongside the rest of KeeperFSMTransition in fsm.h) tags each
+ * NODE_KIND_ANY row with the narrative phase it belongs to, curated once
+ * against every edge's real transition comment. This generator walks
+ * KeeperFSM[] directly, the same way print_fsm_for_graphviz() does -- it
+ * just groups by that field instead of dumping every row into one graph.
+ * If a new NODE_KIND_ANY transition is ever added without setting `phase`,
+ * it defaults to FSM_PHASE_NONE and this generator logs a warning rather
+ * than silently omitting it from every diagram.
+ *
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the PostgreSQL License.
+ *
+ */
+
+#include
+#include
+
+#include "defaults.h"
+#include "fsm.h"
+#include "fsm_mermaid.h"
+#include "log.h"
+#include "state.h"
+
+
+const char *FsmPhaseName[FSM_PHASE_COUNT] = {
+ "none", /* FSM_PHASE_NONE, never requested via CLI */
+ "init",
+ "steady-state",
+ "failover",
+ "maintenance",
+ "removal"
+};
+
+const char *FsmPhaseTitle[FSM_PHASE_COUNT] = {
+ "(none)",
+ "Node init / join",
+ "Steady-state / config changes",
+ "Failover / promotion",
+ "Maintenance",
+ "Node removal / drop"
+};
+
+/* the six colour classes used to shade the diagrams */
+typedef enum
+{
+ FSM_COLOR_META = 0,
+ FSM_COLOR_PRIMARY,
+ FSM_COLOR_SECONDARY,
+ FSM_COLOR_DEMOTING,
+ FSM_COLOR_MAINTENANCE,
+ FSM_COLOR_ELECTION,
+
+ FSM_COLOR_COUNT
+} FsmMermaidColor;
+
+static const char *FsmMermaidColorClassDef[FSM_COLOR_COUNT] = {
+ "classDef metaState fill:#e0e0e0,stroke:#888888,color:#333333",
+ "classDef primaryState fill:#cfe2ff,stroke:#3b6fb6,color:#1a1a1a",
+ "classDef secondaryState fill:#d4edda,stroke:#4c9a5b,color:#1a1a1a",
+ "classDef demotingState fill:#f8d7da,stroke:#c0392b,color:#1a1a1a",
+ "classDef maintenanceState fill:#e8dff5,stroke:#8e6bb0,color:#1a1a1a",
+ "classDef electionState fill:#fff3cd,stroke:#c99a1e,color:#1a1a1a"
+};
+
+static const char *FsmMermaidColorClassName[FSM_COLOR_COUNT] = {
+ "metaState",
+ "primaryState",
+ "secondaryState",
+ "demotingState",
+ "maintenanceState",
+ "electionState"
+};
+
+/*
+ * FsmMermaidColorForState assigns each real state to one of the six colour
+ * classes above. ANY_STATE/NO_STATE never appear as a rendered node (only
+ * as the "from" side of the drop catch-all, which is emitted as a plain
+ * edge, not a node needing a colour), so they're not classified here.
+ */
+static FsmMermaidColor
+FsmMermaidColorForState(NodeState state)
+{
+ switch (state)
+ {
+ case INIT_STATE:
+ case SINGLE_STATE:
+ case DROPPED_STATE:
+ case ANY_STATE:
+ {
+ return FSM_COLOR_META;
+ }
+
+ case PRIMARY_STATE:
+ case WAIT_PRIMARY_STATE:
+ case JOIN_PRIMARY_STATE:
+ case APPLY_SETTINGS_STATE:
+ {
+ return FSM_COLOR_PRIMARY;
+ }
+
+ case SECONDARY_STATE:
+ case CATCHINGUP_STATE:
+ case WAIT_STANDBY_STATE:
+ {
+ return FSM_COLOR_SECONDARY;
+ }
+
+ case DRAINING_STATE:
+ case DEMOTED_STATE:
+ case DEMOTE_TIMEOUT_STATE:
+ {
+ return FSM_COLOR_DEMOTING;
+ }
+
+ case MAINTENANCE_STATE:
+ case PREPARE_MAINTENANCE_STATE:
+ case WAIT_MAINTENANCE_STATE:
+ {
+ return FSM_COLOR_MAINTENANCE;
+ }
+
+ case PREP_PROMOTION_STATE:
+ case STOP_REPLICATION_STATE:
+ case REPORT_LSN_STATE:
+ case FAST_FORWARD_STATE:
+ case JOIN_SECONDARY_STATE:
+ default:
+ {
+ return FSM_COLOR_ELECTION;
+ }
+ }
+}
+
+/*
+ * FsmMermaidId returns the identifier to use for a state in Mermaid source.
+ * NodeStateToString(ANY_STATE) returns "#any state#", which is not a valid
+ * Mermaid identifier (spaces, a '#') -- every other state's string is
+ * already a bare lowercase word and safe to use as-is.
+ */
+static const char *
+FsmMermaidId(NodeState state)
+{
+ if (state == ANY_STATE)
+ {
+ return "any_state";
+ }
+
+ return NodeStateToString(state);
+}
+
+
+bool
+fsm_mermaid_phase_from_string(const char *name, FsmPhase *phase)
+{
+ int i = 0;
+
+ /* start at 1: FSM_PHASE_NONE (index 0) is never a valid CLI argument */
+ for (i = 1; i < FSM_PHASE_COUNT; i++)
+ {
+ if (strcmp(name, FsmPhaseName[i]) == 0)
+ {
+ *phase = (FsmPhase) i;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+/* a small fixed set of "seen" states, big enough for the whole FSM */
+#define FSM_MAX_STATES 32
+
+static bool
+StateSetContains(NodeState *set, int count, NodeState state)
+{
+ int i = 0;
+
+ for (i = 0; i < count; i++)
+ {
+ if (set[i] == state)
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+
+/*
+ * FsmPhaseMaskForState returns a bitmask of every phase (1 << FSM_PHASE_*)
+ * in which the given state appears as either side of a NODE_KIND_ANY edge,
+ * by walking KeeperFSM[] directly. Used to print "also appears in ..."
+ * notes.
+ */
+static int
+FsmPhaseMaskForState(NodeState state)
+{
+ KeeperFSMTransition transition = KeeperFSM[0];
+ int transitionIndex = 0;
+ int mask = 0;
+
+ while (transition.current != NO_STATE)
+ {
+ if (transition.pgKind == NODE_KIND_ANY &&
+ transition.phase != FSM_PHASE_NONE &&
+ transition.current != JOIN_PRIMARY_STATE &&
+ transition.assigned != JOIN_PRIMARY_STATE &&
+ (transition.current == state || transition.assigned == state))
+ {
+ mask |= (1 << transition.phase);
+ }
+
+ transition = KeeperFSM[++transitionIndex];
+ }
+
+ return mask;
+}
+
+
+/*
+ * print_fsm_mermaid_for_phase renders one phase's slice of the FSM as a
+ * Mermaid stateDiagram-v2. Only NODE_KIND_ANY transitions are considered:
+ * Citus coordinator/worker rows never introduce an edge that doesn't also
+ * exist under NODE_KIND_ANY (verified: every one of the 15 Citus-specific
+ * (current, assigned) pairs already has an ANY-kind counterpart), so this
+ * diagram already covers Citus topologies -- there is no separate "Citus"
+ * phase to add.
+ */
+void
+print_fsm_mermaid_for_phase(FsmPhase phase)
+{
+ KeeperFSMTransition transition = KeeperFSM[0];
+ int transitionIndex = 0;
+
+ NodeState seenCurrent[FSM_MAX_STATES] = { 0 };
+ NodeState seenAssigned[FSM_MAX_STATES] = { 0 };
+ int seenCount = 0;
+
+ NodeState diagramStates[FSM_MAX_STATES] = { 0 };
+ int diagramStateCount = 0;
+
+ int i = 0;
+
+ fformat(stdout, "stateDiagram-v2\n");
+
+ while (transition.current != NO_STATE)
+ {
+ bool alreadySeen = false;
+
+ if (transition.pgKind != NODE_KIND_ANY)
+ {
+ /* Citus-specific rows never add a new edge shape, skip them */
+ transition = KeeperFSM[++transitionIndex];
+ continue;
+ }
+
+ if (transition.current == JOIN_PRIMARY_STATE ||
+ transition.assigned == JOIN_PRIMARY_STATE)
+ {
+ /*
+ * join_primary is deprecated and no longer assigned to nodes
+ * (see docs/failover-state-machine.rst) -- KeeperFSM[] still
+ * carries its transitions for backward compatibility with
+ * on-disk state from old versions, but it should not clutter
+ * diagrams describing current behaviour.
+ */
+ transition = KeeperFSM[++transitionIndex];
+ continue;
+ }
+
+ if (transition.phase == FSM_PHASE_NONE)
+ {
+ log_warn("BUG: FSM transition \"%s\" -> \"%s\" (NODE_KIND_ANY) "
+ "has no phase set in KeeperFSM[] (fsm.c) -- it will "
+ "not appear in any phase diagram until this is fixed",
+ NodeStateToString(transition.current),
+ NodeStateToString(transition.assigned));
+ transition = KeeperFSM[++transitionIndex];
+ continue;
+ }
+
+ for (i = 0; i < seenCount; i++)
+ {
+ if (seenCurrent[i] == transition.current &&
+ seenAssigned[i] == transition.assigned)
+ {
+ alreadySeen = true;
+ break;
+ }
+ }
+
+ if (alreadySeen)
+ {
+ transition = KeeperFSM[++transitionIndex];
+ continue;
+ }
+
+ if (seenCount < FSM_MAX_STATES)
+ {
+ seenCurrent[seenCount] = transition.current;
+ seenAssigned[seenCount] = transition.assigned;
+ ++seenCount;
+ }
+
+ if (transition.phase == phase)
+ {
+ fformat(stdout, " %s --> %s : %s\n",
+ FsmMermaidId(transition.current),
+ FsmMermaidId(transition.assigned),
+ transition.comment);
+
+ if (!StateSetContains(diagramStates, diagramStateCount,
+ transition.current) &&
+ diagramStateCount < FSM_MAX_STATES)
+ {
+ diagramStates[diagramStateCount++] = transition.current;
+ }
+ if (!StateSetContains(diagramStates, diagramStateCount,
+ transition.assigned) &&
+ diagramStateCount < FSM_MAX_STATES)
+ {
+ diagramStates[diagramStateCount++] = transition.assigned;
+ }
+ }
+
+ transition = KeeperFSM[++transitionIndex];
+ }
+
+ fformat(stdout, "\n");
+
+ /* notes: make it obvious when a state also lives in another diagram */
+ for (i = 0; i < diagramStateCount; i++)
+ {
+ int mask = FsmPhaseMaskForState(diagramStates[i]);
+ int otherMask = mask & ~(1 << phase);
+ int p = 0;
+ bool first = true;
+ char note[BUFSIZE] = { 0 };
+
+ if (otherMask == 0)
+ {
+ continue;
+ }
+
+ /*
+ * No colon inside the note text itself: Mermaid's "note right of
+ * X : text" grammar treats a colon as the delimiter between the
+ * state name and the note body, and chokes on a second one inside
+ * the body ("Expecting ... got 'DESCR'").
+ */
+ strlcat(note, "also appears in ", BUFSIZE);
+
+ for (p = 0; p < FSM_PHASE_COUNT; p++)
+ {
+ if (otherMask & (1 << p))
+ {
+ if (!first)
+ {
+ strlcat(note, ", ", BUFSIZE);
+ }
+ strlcat(note, FsmPhaseTitle[p], BUFSIZE);
+ first = false;
+ }
+ }
+
+ fformat(stdout, " note right of %s : %s\n",
+ FsmMermaidId(diagramStates[i]), note);
+ }
+
+ fformat(stdout, "\n");
+
+ /* colour scheme: only emit classDef/class for states used here */
+ {
+ bool colorUsed[FSM_COLOR_COUNT] = { false };
+
+ for (i = 0; i < diagramStateCount; i++)
+ {
+ colorUsed[FsmMermaidColorForState(diagramStates[i])] = true;
+ }
+
+ for (i = 0; i < FSM_COLOR_COUNT; i++)
+ {
+ if (colorUsed[i])
+ {
+ fformat(stdout, " %s\n", FsmMermaidColorClassDef[i]);
+ }
+ }
+
+ for (i = 0; i < diagramStateCount; i++)
+ {
+ FsmMermaidColor color = FsmMermaidColorForState(diagramStates[i]);
+
+ fformat(stdout, " class %s %s\n",
+ FsmMermaidId(diagramStates[i]),
+ FsmMermaidColorClassName[color]);
+ }
+ }
+}
diff --git a/src/bin/pg_autoctl/fsm_mermaid.h b/src/bin/pg_autoctl/fsm_mermaid.h
new file mode 100644
index 000000000..5c2b6ada4
--- /dev/null
+++ b/src/bin/pg_autoctl/fsm_mermaid.h
@@ -0,0 +1,29 @@
+/*
+ * src/bin/pg_autoctl/fsm_mermaid.h
+ * Mermaid stateDiagram-v2 rendering of the keeper FSM (KeeperFSM[] in
+ * fsm.c), split into narrative phases so each diagram stays small enough
+ * to read at a glance.
+ *
+ * FsmPhase itself (the enum) lives in fsm.h, as a field directly on
+ * KeeperFSMTransition -- there is exactly one FSM table, and this generator
+ * walks it the same way print_fsm_for_graphviz() does, just grouping by
+ * that field instead of dumping every row.
+ *
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the PostgreSQL License.
+ *
+ */
+
+#ifndef FSM_MERMAID_H
+#define FSM_MERMAID_H
+
+#include "fsm.h"
+#include "state.h"
+
+extern const char *FsmPhaseName[FSM_PHASE_COUNT];
+extern const char *FsmPhaseTitle[FSM_PHASE_COUNT];
+
+void print_fsm_mermaid_for_phase(FsmPhase phase);
+bool fsm_mermaid_phase_from_string(const char *name, FsmPhase *phase);
+
+#endif /* FSM_MERMAID_H */