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 */