From afc361ffd46f260a8f4eb6580230cf222afae79b Mon Sep 17 00:00:00 2001 From: Francisco Miguel Biete Banon Date: Sat, 4 Apr 2026 14:09:43 +0100 Subject: [PATCH] Standby clone support for pgBackRest --- README.md | 2 +- configdata.c | 20 +++++ configfile.h | 2 + doc/cloning-standbys.xml | 93 +++++++++++++++++++++ doc/repmgr-standby-clone.xml | 21 +++-- doc/repmgr.xml | 2 +- repmgr-action-standby.c | 156 +++++++++++++++++++++++++++++++++++ repmgr-client-global.h | 6 +- repmgr-client.c | 6 ++ repmgr-client.h | 2 + repmgr.conf.sample | 7 ++ 11 files changed, 308 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d0af6af1..4759aa60 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ replication, and perform administrative tasks such as failover or switchover operations. The most recent `repmgr` version (5.5.x) supports all PostgreSQL versions from -13 to 17. Despite it could be used with some older ones, some features might not +13 to 18. Despite it could be used with some older ones, some features might not be available, however, it's strongly recommended to use the latest version. `repmgr` is distributed under the GNU GPL 3 and maintained by EnterpriseDB. diff --git a/configdata.c b/configdata.c index 466cc810..b354325e 100644 --- a/configdata.c +++ b/configdata.c @@ -331,6 +331,26 @@ struct ConfigFileSetting config_file_settings[] = { .strmaxlen = sizeof(config_file_options.pg_backupapi_remote_ssh_command) }, {} }, + /* pgbackrest_config_file */ + { + "pgbackrest_config_file", + CONFIG_STRING, + { .strptr = config_file_options.pgbackrest_config_file }, + { .strdefault = "" }, + {}, + { .strmaxlen = sizeof(config_file_options.pgbackrest_config_file) }, + { .postprocess_func = &repmgr_canonicalize_path } + }, + /* pgbackrest_stanza */ + { + "pgbackrest_stanza", + CONFIG_STRING, + { .strptr = config_file_options.pgbackrest_stanza }, + { .strdefault = "" }, + {}, + { .strmaxlen = sizeof(config_file_options.pgbackrest_stanza) }, + {} + }, /* ======================= * standby follow settings diff --git a/configfile.h b/configfile.h index 3d6f423a..69201a02 100644 --- a/configfile.h +++ b/configfile.h @@ -168,6 +168,8 @@ typedef struct char pg_backupapi_host[NAMEDATALEN]; char pg_backupapi_node_name[NAMEDATALEN]; char pg_backupapi_remote_ssh_command[MAXLEN]; + char pgbackrest_config_file[MAXPGPATH]; + char pgbackrest_stanza[NAMEDATALEN]; /* standby promote settings */ int promote_check_timeout; diff --git a/doc/cloning-standbys.xml b/doc/cloning-standbys.xml index 4f4d4b00..35439dab 100644 --- a/doc/cloning-standbys.xml +++ b/doc/cloning-standbys.xml @@ -330,6 +330,99 @@ HINT: after starting the server, you need to register this standby with "repmgr + + Cloning a standby from pgBackRest + + + cloning + from pgBackRest + + + pgBackRest + cloning a standby + + + + can use + pgBackRest + to clone a standby. + + + + pgBackRest support provides the following advantages: + + + + the primary node does not need to perform a new backup every time a + new standby is cloned + + + + + a standby node can be disconnected for longer periods without losing + the ability to catch up, and without causing accumulation of WAL + files on the primary node + + + + + + + Prerequisites for cloning from pgBackRest + + In order to enable pgBackRest support for repmgr standby clone, following + prerequisites must be met: + + + + the pgBackRest archive must include at least one valid backup for this server; + + + + + the pgbackrest_config_file setting in repmgr.conf is set to the full path + to the pgBackRest configuration file; + + + + + the pgbackrest_stanza setting in repmgr.conf is set to the name of the + stanza defined in that configuration file. + + + + + + + For example, repmgr.conf should contain the following entries: + + pgbackrest_config_file='/etc/pgbackrest/pgbackrest.conf' + pgbackrest_stanza='demo' + + + + Note that repmgr standby clone will execute the pgbackrest restore + command using the options defined in the pgBackRest configuration file, without adding + additional options to override them. + + + + To use pgBackRest as a WAL file source, restore_command in repmgr.conf + should be configured to use the pgbackrest archive-get command, e.g.: + + restore_command='pgbackrest --stanza=demo archive-get %f %p' + + + + + pgBackRest support is automatically enabled if pgbackrest_stanza + is set. pgBackRest mode can be disabled using the --without-pgbackrest + command line option. + + + + + Cloning and replication slots diff --git a/doc/repmgr-standby-clone.xml b/doc/repmgr-standby-clone.xml index eac75971..1d987712 100644 --- a/doc/repmgr-standby-clone.xml +++ b/doc/repmgr-standby-clone.xml @@ -18,9 +18,9 @@ repmgr standby clone clones a PostgreSQL node from another PostgreSQL node, typically the primary, but optionally from any other node in - the cluster or from Barman. It creates the replication configuration required - to attach the cloned node to the primary node (or another standby, if cascading replication - is in use). + the cluster or from a backup tool like Barman or pgBackRest. + It creates the replication configuration required to attach the cloned node + to the primary node (or another standby, if cascading replication is in use). @@ -234,7 +234,10 @@ pg_basebackup_options='--waldir=/path/to/wal-directory' &repmgr; supports standbys cloned by another method (e.g. using barman's - barman recover command). + barman recover command, + or using pgBackRest's + pgbackrest restore + command). To integrate the standby as a &repmgr; node, once the standby has been cloned, @@ -467,6 +470,15 @@ pg_basebackup_options='--waldir=/path/to/wal-directory' + + + + + Do not use pgBackRest even if configured. + + + + @@ -484,4 +496,3 @@ pg_basebackup_options='--waldir=/path/to/wal-directory' - diff --git a/doc/repmgr.xml b/doc/repmgr.xml index e8cf9407..39fae8eb 100644 --- a/doc/repmgr.xml +++ b/doc/repmgr.xml @@ -26,7 +26,7 @@ This is the official documentation of &repmgr; &repmgrversion; for - use with PostgreSQL 12 - PostgreSQL 17. + use with PostgreSQL 12 - PostgreSQL 18. &repmgr; is being continually developed and we strongly recommend using the diff --git a/repmgr-action-standby.c b/repmgr-action-standby.c index a55462b0..9dfd501e 100644 --- a/repmgr-action-standby.c +++ b/repmgr-action-standby.c @@ -114,6 +114,7 @@ static void check_recovery_type(PGconn *conn); static void initialise_direct_clone(t_node_info *local_node_record, t_node_info *upstream_node_record); static int run_basebackup(t_node_info *node_record); static int run_file_backup(t_node_info *node_record); +static int run_pgbackrest(t_node_info *local_node_record); static int run_pg_backupapi(t_node_info *node_record); static void copy_configuration_files(bool delete_after_copy); @@ -701,6 +702,9 @@ do_standby_clone(void) case pg_backupapi: log_notice(_("starting backup (using pg_backupapi)...")); break; + case pgbackrest: + log_notice(_("starting backup (using pgBackRest)...")); + break; default: /* should never reach here */ log_error(_("unknown clone mode")); @@ -725,6 +729,9 @@ do_standby_clone(void) case pg_backupapi: r = run_pg_backupapi(&local_node_record); break; + case pgbackrest: + r = run_pgbackrest(&local_node_record); + break; default: /* should never reach here */ log_error(_("unknown clone mode")); @@ -946,6 +953,9 @@ do_standby_clone(void) case pg_backupapi: appendPQExpBufferStr(&event_details, "pg_backupapi"); break; + case pgbackrest: + appendPQExpBufferStr(&event_details, "pgbackrest"); + break; } appendPQExpBuffer(&event_details, @@ -7784,6 +7794,151 @@ run_file_backup(t_node_info *local_node_record) return r; } +static int +run_pgbackrest(t_node_info *local_node_record) +{ + PQExpBufferData command; + int r = SUCCESS; + RecordStatus record_status = RECORD_NOT_FOUND; + + /* Ensure data directory is ready */ + if (runtime_options.dry_run == false) + { + if (!create_pg_dir(local_data_directory, runtime_options.force)) + { + log_error(_("unable to use directory \"%s\""), local_data_directory); + log_hint(_("use -F/--force to force this directory to be overwritten")); + return ERR_BAD_CONFIG; + } + } + + initPQExpBuffer(&command); + + appendPQExpBufferStr(&command, "pgbackrest"); + + if (*config_file_options.pgbackrest_config_file != '\0') + { + appendPQExpBuffer(&command, " --config=%s", config_file_options.pgbackrest_config_file); + } + + appendPQExpBuffer(&command, " --stanza=%s", config_file_options.pgbackrest_stanza); + + appendPQExpBufferStr(&command, " restore"); + + if (runtime_options.dry_run == true) + { + log_info(_("would execute:\n %s"), command.data); + termPQExpBuffer(&command); + return SUCCESS; + } + + log_info(_("executing:\n %s"), command.data); + + if (local_command(command.data, NULL) == false) + { + r = ERR_BAD_BASEBACKUP; + } + + termPQExpBuffer(&command); + + /* If restore failed, return early */ + if (r != SUCCESS) + return r; + + /* + * if replication slots in use, create replication slot + */ + if (config_file_options.use_replication_slots == true) + { + bool slot_warning = false; + + if (runtime_options.no_upstream_connection == true) + { + slot_warning = true; + } + else + { + t_node_info upstream_node_record = T_NODE_INFO_INITIALIZER; + t_replication_slot slot_info = T_REPLICATION_SLOT_INITIALIZER; + PGconn *upstream_conn = NULL; + + /* check connections are still available */ + (void) connection_ping_reconnect(primary_conn); + + if (source_conn != primary_conn) + (void) connection_ping_reconnect(source_conn); + + record_status = get_node_record(source_conn, upstream_node_id, &upstream_node_record); + + if (record_status != RECORD_FOUND) + { + log_error(_("unable to retrieve node record for upstream node %i"), upstream_node_id); + slot_warning = true; + } + else + { + upstream_conn = establish_db_connection(upstream_node_record.conninfo, false); + if (PQstatus(upstream_conn) != CONNECTION_OK) + { + log_error(_("unable to connect to upstream node %i to create a replication slot"), upstream_node_id); + slot_warning = true; + } + else + { + record_status = get_slot_record(upstream_conn, local_node_record->slot_name, &slot_info); + + if (record_status == RECORD_FOUND) + { + log_verbose(LOG_INFO, + _("replication slot \"%s\" already exists on upstream node %i"), + local_node_record->slot_name, + upstream_node_id); + } + else + { + PQExpBufferData errmsg; + bool success; + + initPQExpBuffer(&errmsg); + success = create_replication_slot(upstream_conn, + local_node_record->slot_name, + &upstream_node_record, + &errmsg); + if (success == false) + { + log_error(_("unable to create replication slot \"%s\" on upstream node %i"), + local_node_record->slot_name, + upstream_node_id); + log_detail("%s", errmsg.data); + slot_warning = true; + } + else + { + log_notice(_("replication slot \"%s\" created on upstream node \"%s\" (ID: %i)"), + local_node_record->slot_name, + upstream_node_record.node_name, + upstream_node_id); + } + termPQExpBuffer(&errmsg); + } + + PQfinish(upstream_conn); + } + } + } + + if (slot_warning == true) + { + log_warning(_("\"use_replication_slots\" specified but a replication slot could not be created")); + log_hint(_("ensure a replication slot called \"%s\" is created on the upstream node (ID: %i)"), + local_node_record->slot_name, + upstream_node_id); + } + } + + return r; +} + /* * Perform a call to pg_backupapi endpoint to ask barman to write the backup @@ -9243,6 +9398,7 @@ do_standby_help(void) printf(_(" --verify-backup verify a cloned node using the \"pg_verifybackup\" utility\n")); #endif printf(_(" --without-barman do not clone from Barman even if configured\n")); + printf(_(" --without-pgbackrest do not clone from pgBackRest even if configured\n")); printf(_(" --replication-conf-only generate replication configuration for a previously cloned instance\n")); printf(_(" --recovery-min-apply-delay set PostgreSQL configuration parameter \"recovery_min_apply_delay\"\n" \ " (overrides any setting in repmgr.conf)\n")); diff --git a/repmgr-client-global.h b/repmgr-client-global.h index 5ed4f0f2..d932700a 100644 --- a/repmgr-client-global.h +++ b/repmgr-client-global.h @@ -88,6 +88,7 @@ typedef struct char replication_user[MAXLEN]; char upstream_conninfo[MAXLEN]; bool without_barman; + bool without_pgbackrest; bool replication_conf_only; bool verify_backup; @@ -166,7 +167,7 @@ typedef struct UNKNOWN_NODE_ID, "", "", UNKNOWN_NODE_ID, \ /* "standby clone" options */ \ false, CONFIG_FILE_SAMEPATH, false, false, false, "", "", "", \ - false, false, false, \ + false, false, false, false, \ /* "standby clone"/"standby follow" options */ \ NO_UPSTREAM_NODE, \ /* "standby register" options */ \ @@ -194,7 +195,8 @@ typedef enum { barman, pg_basebackup, - pg_backupapi + pg_backupapi, + pgbackrest } standy_clone_mode; typedef enum diff --git a/repmgr-client.c b/repmgr-client.c index 022ccec9..4439b3c8 100644 --- a/repmgr-client.c +++ b/repmgr-client.c @@ -444,6 +444,10 @@ main(int argc, char **argv) runtime_options.without_barman = true; break; + case OPT_WITHOUT_PGBACKREST: + runtime_options.without_pgbackrest = true; + break; + case OPT_REPLICATION_CONF_ONLY: runtime_options.replication_conf_only = true; break; @@ -3105,6 +3109,8 @@ get_standby_clone_mode(void) log_info("Attempting to use `pg_backupapi` new restore mode"); mode = pg_backupapi; } + else if (*config_file_options.pgbackrest_stanza != '\0' && runtime_options.without_pgbackrest == false) + mode = pgbackrest; else mode = pg_basebackup; } diff --git a/repmgr-client.h b/repmgr-client.h index 8ce3451b..6f908f74 100644 --- a/repmgr-client.h +++ b/repmgr-client.h @@ -101,6 +101,7 @@ #define OPT_VERIFY_BACKUP 1048 #define OPT_RECOVERY_MIN_APPLY_DELAY 1049 #define OPT_REPMGRD 1050 +#define OPT_WITHOUT_PGBACKREST 1051 /* These options are for internal use only */ #define OPT_CONFIG_ARCHIVE_DIR 2001 @@ -164,6 +165,7 @@ static struct option long_options[] = {"upstream-conninfo", required_argument, NULL, OPT_UPSTREAM_CONNINFO}, {"upstream-node-id", required_argument, NULL, OPT_UPSTREAM_NODE_ID}, {"without-barman", no_argument, NULL, OPT_WITHOUT_BARMAN}, + {"without-pgbackrest", no_argument, NULL, OPT_WITHOUT_PGBACKREST}, {"replication-conf-only", no_argument, NULL, OPT_REPLICATION_CONF_ONLY}, {"verify-backup", no_argument, NULL, OPT_VERIFY_BACKUP }, {"recovery-min-apply-delay", required_argument, NULL, OPT_RECOVERY_MIN_APPLY_DELAY }, diff --git a/repmgr.conf.sample b/repmgr.conf.sample index abc1b4e2..744941d3 100644 --- a/repmgr.conf.sample +++ b/repmgr.conf.sample @@ -280,6 +280,13 @@ ssh_options='-q -o ConnectTimeout=10' # Options to append to "ssh" # Barman server (needed if the file is # in a non-standard location) +#------------------------------------------------------------------------------ +# pgBackRest options +#------------------------------------------------------------------------------ + +#pgbackrest_config_file='' # The full path to pgBackRest configuration file +#pgbackrest_stanza='' # The pgBackRest stanza name to use + #------------------------------------------------------------------------------ # Failover and monitoring settings (repmgrd) #------------------------------------------------------------------------------