rebased -- Nathan Bossart Amazon Web Services: https://aws.amazon.com
>From 046d6e4b13d3a6b15df1245f3154969f7553594d Mon Sep 17 00:00:00 2001 From: Nathan Bossart <nathandboss...@gmail.com> Date: Wed, 15 Feb 2023 14:28:53 -0800 Subject: [PATCH v22 1/5] introduce routine for checking mutually exclusive string GUCs
--- src/backend/postmaster/pgarch.c | 8 +++----- src/backend/utils/misc/guc.c | 22 ++++++++++++++++++++++ src/include/utils/guc.h | 3 +++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index d82bcc2cfd..98a5aa3661 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -912,11 +912,9 @@ LoadArchiveLibrary(void) { ArchiveModuleInit archive_init; - if (XLogArchiveLibrary[0] != '\0' && XLogArchiveCommand[0] != '\0') - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("both archive_command and archive_library set"), - errdetail("Only one of archive_command, archive_library may be set."))); + (void) CheckMutuallyExclusiveStringGUCs(XLogArchiveLibrary, "archive_library", + XLogArchiveCommand, "archive_command", + ERROR); /* * If shell archiving is enabled, use our special initialization function. diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 3fb6803998..02bc4d66e7 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -2659,6 +2659,28 @@ ReportGUCOption(struct config_generic *record) pfree(val); } +/* + * If both parameters are set, emits a log message at 'elevel' and returns + * false. Otherwise, returns true. + */ +bool +CheckMutuallyExclusiveStringGUCs(const char *p1val, const char *p1name, + const char *p2val, const char *p2name, + int elevel) +{ + if (p1val[0] != '\0' && p2val[0] != '\0') + { + ereport(elevel, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("cannot set both %s and %s", p1name, p2name), + errdetail("Only one of %s or %s may be set.", + p1name, p2name))); + return false; + } + + return true; +} + /* * Convert a value from one of the human-friendly units ("kB", "min" etc.) * to the given base unit. 'value' and 'unit' are the input value and unit diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h index e4a594b5e8..018bb7e55b 100644 --- a/src/include/utils/guc.h +++ b/src/include/utils/guc.h @@ -376,6 +376,9 @@ extern void RestrictSearchPath(void); extern void AtEOXact_GUC(bool isCommit, int nestLevel); extern void BeginReportingGUCOptions(void); extern void ReportChangedGUCOptions(void); +extern bool CheckMutuallyExclusiveStringGUCs(const char *p1val, const char *p1name, + const char *p2val, const char *p2name, + int elevel); extern void ParseLongOption(const char *string, char **name, char **value); extern const char *get_config_unit_name(int flags); extern bool parse_int(const char *value, int *result, int flags, -- 2.25.1
>From 88e5e696792efdb62f7067400223c67357db6dba Mon Sep 17 00:00:00 2001 From: Nathan Bossart <nathandboss...@gmail.com> Date: Wed, 15 Feb 2023 10:36:00 -0800 Subject: [PATCH v22 2/5] refactor code for restoring via shell --- src/backend/Makefile | 2 +- src/backend/access/transam/timeline.c | 12 +- src/backend/access/transam/xlog.c | 50 ++++- src/backend/access/transam/xlogarchive.c | 167 ++++----------- src/backend/access/transam/xlogrecovery.c | 3 +- src/backend/meson.build | 1 + src/backend/postmaster/startup.c | 16 +- src/backend/restore/Makefile | 18 ++ src/backend/restore/meson.build | 5 + src/backend/restore/shell_restore.c | 245 ++++++++++++++++++++++ src/include/Makefile | 2 +- src/include/access/xlogarchive.h | 9 +- src/include/meson.build | 1 + src/include/postmaster/startup.h | 1 + src/include/restore/shell_restore.h | 26 +++ src/tools/pgindent/typedefs.list | 1 + 16 files changed, 407 insertions(+), 152 deletions(-) create mode 100644 src/backend/restore/Makefile create mode 100644 src/backend/restore/meson.build create mode 100644 src/backend/restore/shell_restore.c create mode 100644 src/include/restore/shell_restore.h diff --git a/src/backend/Makefile b/src/backend/Makefile index 6700aec039..590b5002c0 100644 --- a/src/backend/Makefile +++ b/src/backend/Makefile @@ -19,7 +19,7 @@ include $(top_builddir)/src/Makefile.global SUBDIRS = access archive backup bootstrap catalog parser commands executor \ foreign lib libpq \ main nodes optimizer partitioning port postmaster \ - regex replication rewrite \ + regex replication restore rewrite \ statistics storage tcop tsearch utils $(top_builddir)/src/timezone \ jit diff --git a/src/backend/access/transam/timeline.c b/src/backend/access/transam/timeline.c index 146751ae37..3269547de7 100644 --- a/src/backend/access/transam/timeline.c +++ b/src/backend/access/transam/timeline.c @@ -59,7 +59,8 @@ restoreTimeLineHistoryFiles(TimeLineID begin, TimeLineID end) continue; TLHistoryFileName(histfname, tli); - if (RestoreArchivedFile(path, histfname, "RECOVERYHISTORY", 0, false)) + if (RestoreArchivedFile(path, histfname, "RECOVERYHISTORY", 0, false, + ARCHIVE_TYPE_TIMELINE_HISTORY)) KeepFileRestoredFromArchive(path, histfname); } } @@ -97,7 +98,8 @@ readTimeLineHistory(TimeLineID targetTLI) { TLHistoryFileName(histfname, targetTLI); fromArchive = - RestoreArchivedFile(path, histfname, "RECOVERYHISTORY", 0, false); + RestoreArchivedFile(path, histfname, "RECOVERYHISTORY", 0, false, + ARCHIVE_TYPE_TIMELINE_HISTORY); } else TLHistoryFilePath(path, targetTLI); @@ -232,7 +234,8 @@ existsTimeLineHistory(TimeLineID probeTLI) if (ArchiveRecoveryRequested) { TLHistoryFileName(histfname, probeTLI); - RestoreArchivedFile(path, histfname, "RECOVERYHISTORY", 0, false); + RestoreArchivedFile(path, histfname, "RECOVERYHISTORY", 0, false, + ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS); } else TLHistoryFilePath(path, probeTLI); @@ -334,7 +337,8 @@ writeTimeLineHistory(TimeLineID newTLI, TimeLineID parentTLI, if (ArchiveRecoveryRequested) { TLHistoryFileName(histfname, parentTLI); - RestoreArchivedFile(path, histfname, "RECOVERYHISTORY", 0, false); + RestoreArchivedFile(path, histfname, "RECOVERYHISTORY", 0, false, + ARCHIVE_TYPE_TIMELINE_HISTORY); } else TLHistoryFilePath(path, parentTLI); diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index c3fd9c1eae..1eb6d57fe2 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -83,6 +83,7 @@ #include "replication/snapbuild.h" #include "replication/walreceiver.h" #include "replication/walsender.h" +#include "restore/shell_restore.h" #include "storage/bufmgr.h" #include "storage/fd.h" #include "storage/ipc.h" @@ -704,6 +705,7 @@ static char *GetXLogBuffer(XLogRecPtr ptr, TimeLineID tli); static XLogRecPtr XLogBytePosToRecPtr(uint64 bytepos); static XLogRecPtr XLogBytePosToEndRecPtr(uint64 bytepos); static uint64 XLogRecPtrToBytePos(XLogRecPtr ptr); +static void GetOldestRestartPointFileName(char *fname); static void WALInsertLockAcquire(void); static void WALInsertLockAcquireExclusive(void); @@ -5257,12 +5259,18 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog, { /* * Execute the recovery_end_command, if any. + * + * The command is provided the archive file cutoff point for use during + * log shipping replication. All files earlier than this point can be + * deleted from the archive, though there is no requirement to do so. */ - if (recoveryEndCommand && strcmp(recoveryEndCommand, "") != 0) - ExecuteRecoveryCommand(recoveryEndCommand, - "recovery_end_command", - true, - WAIT_EVENT_RECOVERY_END_COMMAND); + if (shell_recovery_end_configured()) + { + char lastRestartPointFname[MAXFNAMELEN]; + + GetOldestRestartPointFileName(lastRestartPointFname); + shell_recovery_end(lastRestartPointFname); + } /* * We switched to a new timeline. Clean up segments on the old timeline. @@ -7753,12 +7761,18 @@ CreateRestartPoint(int flags) /* * Finally, execute archive_cleanup_command, if any. + * + * The command is provided the archive file cutoff point for use during + * log shipping replication. All files earlier than this point can be + * deleted from the archive, though there is no requirement to do so. */ - if (archiveCleanupCommand && strcmp(archiveCleanupCommand, "") != 0) - ExecuteRecoveryCommand(archiveCleanupCommand, - "archive_cleanup_command", - false, - WAIT_EVENT_ARCHIVE_CLEANUP_COMMAND); + if (shell_archive_cleanup_configured()) + { + char lastRestartPointFname[MAXFNAMELEN]; + + GetOldestRestartPointFileName(lastRestartPointFname); + shell_archive_cleanup(lastRestartPointFname); + } return true; } @@ -9388,6 +9402,22 @@ GetOldestRestartPoint(XLogRecPtr *oldrecptr, TimeLineID *oldtli) LWLockRelease(ControlFileLock); } +/* + * Returns the WAL file name for the last checkpoint or restartpoint. This is + * the oldest WAL file that we still need if we have to restart recovery. + */ +static void +GetOldestRestartPointFileName(char *fname) +{ + XLogRecPtr restartRedoPtr; + TimeLineID restartTli; + XLogSegNo restartSegNo; + + GetOldestRestartPoint(&restartRedoPtr, &restartTli); + XLByteToSeg(restartRedoPtr, restartSegNo, wal_segment_size); + XLogFileName(fname, restartTli, restartSegNo, wal_segment_size); +} + /* Thin wrapper around ShutdownWalRcv(). */ void XLogShutdownWalRcv(void) diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c index caa1f03d93..5756f52124 100644 --- a/src/backend/access/transam/xlogarchive.c +++ b/src/backend/access/transam/xlogarchive.c @@ -23,12 +23,12 @@ #include "access/xlog_internal.h" #include "access/xlogarchive.h" #include "common/archive.h" -#include "common/percentrepl.h" #include "miscadmin.h" #include "pgstat.h" #include "postmaster/pgarch.h" #include "postmaster/startup.h" #include "replication/walsender.h" +#include "restore/shell_restore.h" #include "storage/fd.h" #include "storage/ipc.h" @@ -53,12 +53,11 @@ bool RestoreArchivedFile(char *path, const char *xlogfname, const char *recovername, off_t expectedSize, - bool cleanupEnabled) + bool cleanupEnabled, ArchiveType archive_type) { char xlogpath[MAXPGPATH]; - char *xlogRestoreCmd; char lastRestartPointFname[MAXPGPATH]; - int rc; + bool ret = false; struct stat stat_buf; XLogSegNo restartSegNo; XLogRecPtr restartRedoPtr; @@ -72,8 +71,21 @@ RestoreArchivedFile(char *path, const char *xlogfname, goto not_available; /* In standby mode, restore_command might not be supplied */ - if (recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0) - goto not_available; + switch (archive_type) + { + case ARCHIVE_TYPE_WAL_SEGMENT: + if (!shell_restore_file_configured()) + goto not_available; + break; + case ARCHIVE_TYPE_TIMELINE_HISTORY: + if (!shell_restore_file_configured()) + goto not_available; + break; + case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS: + if (!shell_restore_file_configured()) + goto not_available; + break; + } /* * When doing archive recovery, we always prefer an archived log file even @@ -149,39 +161,33 @@ RestoreArchivedFile(char *path, const char *xlogfname, else XLogFileName(lastRestartPointFname, 0, 0, wal_segment_size); - /* Build the restore command to execute */ - xlogRestoreCmd = BuildRestoreCommand(recoveryRestoreCommand, - xlogpath, xlogfname, - lastRestartPointFname); - - ereport(DEBUG3, - (errmsg_internal("executing restore command \"%s\"", - xlogRestoreCmd))); - - fflush(NULL); - pgstat_report_wait_start(WAIT_EVENT_RESTORE_COMMAND); - /* - * PreRestoreCommand() informs the SIGTERM handler for the startup process - * that it should proc_exit() right away. This is done for the duration - * of the system() call because there isn't a good way to break out while - * it is executing. Since we might call proc_exit() in a signal handler, - * it is best to put any additional logic before or after the - * PreRestoreCommand()/PostRestoreCommand() section. + * To ensure we are responsive to server shutdown, check for shutdown + * requests before and after restoring a file. If there is one, we exit + * right away. */ - PreRestoreCommand(); + HandleStartupProcShutdownRequests(); /* * Copy xlog from archival storage to XLOGDIR */ - rc = system(xlogRestoreCmd); - - PostRestoreCommand(); + switch (archive_type) + { + case ARCHIVE_TYPE_WAL_SEGMENT: + ret = shell_restore_wal_segment(xlogfname, xlogpath, + lastRestartPointFname); + break; + case ARCHIVE_TYPE_TIMELINE_HISTORY: + ret = shell_restore_timeline_history(xlogfname, xlogpath); + break; + case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS: + ret = shell_restore_timeline_history(xlogfname, xlogpath); + break; + } - pgstat_report_wait_end(); - pfree(xlogRestoreCmd); + HandleStartupProcShutdownRequests(); - if (rc == 0) + if (ret) { /* * command apparently succeeded, but let's make sure the file is @@ -237,37 +243,6 @@ RestoreArchivedFile(char *path, const char *xlogfname, } } - /* - * Remember, we rollforward UNTIL the restore fails so failure here is - * just part of the process... that makes it difficult to determine - * whether the restore failed because there isn't an archive to restore, - * or because the administrator has specified the restore program - * incorrectly. We have to assume the former. - * - * However, if the failure was due to any sort of signal, it's best to - * punt and abort recovery. (If we "return false" here, upper levels will - * assume that recovery is complete and start up the database!) It's - * essential to abort on child SIGINT and SIGQUIT, because per spec - * system() ignores SIGINT and SIGQUIT while waiting; if we see one of - * those it's a good bet we should have gotten it too. - * - * On SIGTERM, assume we have received a fast shutdown request, and exit - * cleanly. It's pure chance whether we receive the SIGTERM first, or the - * child process. If we receive it first, the signal handler will call - * proc_exit, otherwise we do it here. If we or the child process received - * SIGTERM for any other reason than a fast shutdown request, postmaster - * will perform an immediate shutdown when it sees us exiting - * unexpectedly. - * - * We treat hard shell errors such as "command not found" as fatal, too. - */ - if (wait_result_is_signal(rc, SIGTERM)) - proc_exit(1); - - ereport(wait_result_is_any_signal(rc, true) ? FATAL : DEBUG2, - (errmsg("could not restore file \"%s\" from archive: %s", - xlogfname, wait_result_to_str(rc)))); - not_available: /* @@ -281,74 +256,6 @@ not_available: return false; } -/* - * Attempt to execute an external shell command during recovery. - * - * 'command' is the shell command to be executed, 'commandName' is a - * human-readable name describing the command emitted in the logs. If - * 'failOnSignal' is true and the command is killed by a signal, a FATAL - * error is thrown. Otherwise a WARNING is emitted. - * - * This is currently used for recovery_end_command and archive_cleanup_command. - */ -void -ExecuteRecoveryCommand(const char *command, const char *commandName, - bool failOnSignal, uint32 wait_event_info) -{ - char *xlogRecoveryCmd; - char lastRestartPointFname[MAXPGPATH]; - int rc; - XLogSegNo restartSegNo; - XLogRecPtr restartRedoPtr; - TimeLineID restartTli; - - Assert(command && commandName); - - /* - * Calculate the archive file cutoff point for use during log shipping - * replication. All files earlier than this point can be deleted from the - * archive, though there is no requirement to do so. - */ - GetOldestRestartPoint(&restartRedoPtr, &restartTli); - XLByteToSeg(restartRedoPtr, restartSegNo, wal_segment_size); - XLogFileName(lastRestartPointFname, restartTli, restartSegNo, - wal_segment_size); - - /* - * construct the command to be executed - */ - xlogRecoveryCmd = replace_percent_placeholders(command, commandName, "r", lastRestartPointFname); - - ereport(DEBUG3, - (errmsg_internal("executing %s \"%s\"", commandName, command))); - - /* - * execute the constructed command - */ - fflush(NULL); - pgstat_report_wait_start(wait_event_info); - rc = system(xlogRecoveryCmd); - pgstat_report_wait_end(); - - pfree(xlogRecoveryCmd); - - if (rc != 0) - { - /* - * If the failure was due to any sort of signal, it's best to punt and - * abort recovery. See comments in RestoreArchivedFile(). - */ - ereport((failOnSignal && wait_result_is_any_signal(rc, true)) ? FATAL : WARNING, - /*------ - translator: First %s represents a postgresql.conf parameter name like - "recovery_end_command", the 2nd is the value of that parameter, the - third an already translated error message. */ - (errmsg("%s \"%s\": %s", commandName, - command, wait_result_to_str(rc)))); - } -} - - /* * A file was restored from the archive under a temporary filename (path), * and now we want to keep it. Rename it under the permanent filename in diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c index 29c5bec084..07213b1897 100644 --- a/src/backend/access/transam/xlogrecovery.c +++ b/src/backend/access/transam/xlogrecovery.c @@ -4209,7 +4209,8 @@ XLogFileRead(XLogSegNo segno, int emode, TimeLineID tli, if (!RestoreArchivedFile(path, xlogfname, "RECOVERYXLOG", wal_segment_size, - InRedo)) + InRedo, + ARCHIVE_TYPE_WAL_SEGMENT)) return -1; break; diff --git a/src/backend/meson.build b/src/backend/meson.build index 436c04af08..42ff1b303d 100644 --- a/src/backend/meson.build +++ b/src/backend/meson.build @@ -27,6 +27,7 @@ subdir('port') subdir('postmaster') subdir('regex') subdir('replication') +subdir('restore') subdir('rewrite') subdir('statistics') subdir('storage') diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index ef6f98ebcd..cc665ce259 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -169,8 +169,7 @@ HandleStartupProcInterrupts(void) /* * Check if we were requested to exit without finishing recovery. */ - if (shutdown_requested) - proc_exit(1); + HandleStartupProcShutdownRequests(); /* * Emergency bailout if postmaster has died. This is to avoid the @@ -194,6 +193,16 @@ HandleStartupProcInterrupts(void) ProcessLogMemoryContextInterrupt(); } +/* + * If there is a pending shutdown request, exit. + */ +void +HandleStartupProcShutdownRequests(void) +{ + if (shutdown_requested) + proc_exit(1); +} + /* -------------------------------- * signal handler routines @@ -274,8 +283,7 @@ PreRestoreCommand(void) * shutdown request received just before this. */ in_restore_command = true; - if (shutdown_requested) - proc_exit(1); + HandleStartupProcShutdownRequests(); } void diff --git a/src/backend/restore/Makefile b/src/backend/restore/Makefile new file mode 100644 index 0000000000..983d8e980f --- /dev/null +++ b/src/backend/restore/Makefile @@ -0,0 +1,18 @@ +#------------------------------------------------------------------------- +# +# Makefile-- +# Makefile for src/backend/restore +# +# IDENTIFICATION +# src/backend/restore/Makefile +# +#------------------------------------------------------------------------- + +subdir = src/backend/restore +top_builddir = ../../.. +include $(top_builddir)/src/Makefile.global + +OBJS = \ + shell_restore.o + +include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/restore/meson.build b/src/backend/restore/meson.build new file mode 100644 index 0000000000..e4275b3d9c --- /dev/null +++ b/src/backend/restore/meson.build @@ -0,0 +1,5 @@ +# Copyright (c) 2024, PostgreSQL Global Development Group + +backend_sources += files( + 'shell_restore.c' +) diff --git a/src/backend/restore/shell_restore.c b/src/backend/restore/shell_restore.c new file mode 100644 index 0000000000..42291625c1 --- /dev/null +++ b/src/backend/restore/shell_restore.c @@ -0,0 +1,245 @@ +/*------------------------------------------------------------------------- + * + * shell_restore.c + * + * These restore functions use a user-specified shell command (e.g., the + * restore_command GUC). + * + * Copyright (c) 2024, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/backend/restore/shell_restore.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include <signal.h> + +#include "access/xlog.h" +#include "access/xlogrecovery.h" +#include "access/xlog_internal.h" +#include "common/archive.h" +#include "common/percentrepl.h" +#include "postmaster/startup.h" +#include "restore/shell_restore.h" +#include "storage/ipc.h" +#include "utils/wait_event.h" + +static bool shell_restore_file(const char *file, const char *path, + const char *lastRestartPointFileName); +static void ExecuteRecoveryCommand(const char *command, + const char *commandName, + bool failOnSignal, + uint32 wait_event_info, + const char *lastRestartPointFileName); + +/* + * Attempt to execute a shell-based restore command to retrieve a WAL segment. + * + * Returns true if the command has succeeded, false otherwise. + */ +bool +shell_restore_wal_segment(const char *file, const char *path, + const char *lastRestartPointFileName) +{ + return shell_restore_file(file, path, lastRestartPointFileName); +} + +/* + * Attempt to execute a shell-based restore command to retrieve a timeline + * history file. + * + * Returns true if the command has succeeded, false otherwise. + */ +bool +shell_restore_timeline_history(const char *file, const char *path) +{ + char lastRestartPointFname[MAXPGPATH]; + + XLogFileName(lastRestartPointFname, 0, 0L, wal_segment_size); + return shell_restore_file(file, path, lastRestartPointFname); +} + +/* + * Check whether restore_command is supplied. + */ +bool +shell_restore_file_configured(void) +{ + return recoveryRestoreCommand[0] != '\0'; +} + +/* + * Attempt to execute a shell-based restore command to retrieve a file. + * + * Returns true if the command has succeeded, false otherwise. + */ +static bool +shell_restore_file(const char *file, const char *path, + const char *lastRestartPointFileName) +{ + char *cmd; + int rc; + + /* Build the restore command to execute */ + cmd = BuildRestoreCommand(recoveryRestoreCommand, path, file, + lastRestartPointFileName); + + ereport(DEBUG3, + (errmsg_internal("executing restore command \"%s\"", cmd))); + + fflush(NULL); + pgstat_report_wait_start(WAIT_EVENT_RESTORE_COMMAND); + + /* + * PreRestoreCommand() informs the SIGTERM handler for the startup process + * that it should proc_exit() right away. This is done for the duration + * of the system() call because there isn't a good way to break out while + * it is executing. Since we might call proc_exit() in a signal handler, + * it is best to put any additional logic before or after the + * PreRestoreCommand()/PostRestoreCommand() section. + */ + PreRestoreCommand(); + + /* + * Copy xlog from archival storage to XLOGDIR + */ + rc = system(cmd); + + PostRestoreCommand(); + + pgstat_report_wait_end(); + pfree(cmd); + + /* + * Remember, we rollforward UNTIL the restore fails so failure here is + * just part of the process... that makes it difficult to determine + * whether the restore failed because there isn't an archive to restore, + * or because the administrator has specified the restore program + * incorrectly. We have to assume the former. + * + * However, if the failure was due to any sort of signal, it's best to + * punt and abort recovery. (If we "return false" here, upper levels will + * assume that recovery is complete and start up the database!) It's + * essential to abort on child SIGINT and SIGQUIT, because per spec + * system() ignores SIGINT and SIGQUIT while waiting; if we see one of + * those it's a good bet we should have gotten it too. + * + * On SIGTERM, assume we have received a fast shutdown request, and exit + * cleanly. It's pure chance whether we receive the SIGTERM first, or the + * child process. If we receive it first, the signal handler will call + * proc_exit, otherwise we do it here. If we or the child process received + * SIGTERM for any other reason than a fast shutdown request, postmaster + * will perform an immediate shutdown when it sees us exiting + * unexpectedly. + * + * We treat hard shell errors such as "command not found" as fatal, too. + */ + if (rc != 0) + { + if (wait_result_is_signal(rc, SIGTERM)) + proc_exit(1); + + ereport(wait_result_is_any_signal(rc, true) ? FATAL : DEBUG2, + (errmsg("could not restore file \"%s\" from archive: %s", + file, wait_result_to_str(rc)))); + } + + return (rc == 0); +} + +/* + * Check whether archive_cleanup_command is supplied. + */ +bool +shell_archive_cleanup_configured(void) +{ + return archiveCleanupCommand[0] != '\0'; +} + +/* + * Attempt to execute a shell-based archive cleanup command. + */ +void +shell_archive_cleanup(const char *lastRestartPointFileName) +{ + ExecuteRecoveryCommand(archiveCleanupCommand, "archive_cleanup_command", + false, WAIT_EVENT_ARCHIVE_CLEANUP_COMMAND, + lastRestartPointFileName); +} + +/* + * Check whether recovery_end_command is supplied. + */ +bool +shell_recovery_end_configured(void) +{ + return recoveryEndCommand[0] != '\0'; +} + +/* + * Attempt to execute a shell-based end-of-recovery command. + */ +void +shell_recovery_end(const char *lastRestartPointFileName) +{ + ExecuteRecoveryCommand(recoveryEndCommand, "recovery_end_command", true, + WAIT_EVENT_RECOVERY_END_COMMAND, + lastRestartPointFileName); +} + +/* + * Attempt to execute an external shell command during recovery. + * + * 'command' is the shell command to be executed, 'commandName' is a + * human-readable name describing the command emitted in the logs. If + * 'failOnSignal' is true and the command is killed by a signal, a FATAL + * error is thrown. Otherwise a WARNING is emitted. + * + * This is currently used for recovery_end_command and archive_cleanup_command. + */ +static void +ExecuteRecoveryCommand(const char *command, const char *commandName, + bool failOnSignal, uint32 wait_event_info, + const char *lastRestartPointFileName) +{ + char *xlogRecoveryCmd; + int rc; + + Assert(command && commandName); + + /* + * construct the command to be executed + */ + xlogRecoveryCmd = replace_percent_placeholders(command, commandName, "r", + lastRestartPointFileName); + + ereport(DEBUG3, + (errmsg_internal("executing %s \"%s\"", commandName, command))); + + /* + * execute the constructed command + */ + fflush(NULL); + pgstat_report_wait_start(wait_event_info); + rc = system(xlogRecoveryCmd); + pgstat_report_wait_end(); + + pfree(xlogRecoveryCmd); + + if (rc != 0) + { + /* + * If the failure was due to any sort of signal, it's best to punt and + * abort recovery. See comments in shell_restore(). + */ + ereport((failOnSignal && wait_result_is_any_signal(rc, true)) ? FATAL : WARNING, + /*------ + translator: First %s represents a postgresql.conf parameter name like + "recovery_end_command", the 2nd is the value of that parameter, the + third an already translated error message. */ + (errmsg("%s \"%s\": %s", commandName, + command, wait_result_to_str(rc)))); + } +} diff --git a/src/include/Makefile b/src/include/Makefile index b8b576a4de..7e20f3e4c2 100644 --- a/src/include/Makefile +++ b/src/include/Makefile @@ -20,7 +20,7 @@ all: pg_config.h pg_config_ext.h pg_config_os.h SUBDIRS = access archive bootstrap catalog commands common datatype \ executor fe_utils foreign jit \ lib libpq mb nodes optimizer parser partitioning postmaster \ - regex replication rewrite \ + regex replication restore rewrite \ statistics storage tcop snowball snowball/libstemmer tsearch \ tsearch/dicts utils port port/atomics port/win32 port/win32_msvc \ port/win32_msvc/sys port/win32/arpa port/win32/netinet \ diff --git a/src/include/access/xlogarchive.h b/src/include/access/xlogarchive.h index 0701475fb4..67e0fdcdec 100644 --- a/src/include/access/xlogarchive.h +++ b/src/include/access/xlogarchive.h @@ -17,9 +17,16 @@ #include "access/xlogdefs.h" +typedef enum ArchiveType +{ + ARCHIVE_TYPE_WAL_SEGMENT, + ARCHIVE_TYPE_TIMELINE_HISTORY, + ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS +} ArchiveType; + extern bool RestoreArchivedFile(char *path, const char *xlogfname, const char *recovername, off_t expectedSize, - bool cleanupEnabled); + bool cleanupEnabled, ArchiveType archive_type); extern void ExecuteRecoveryCommand(const char *command, const char *commandName, bool failOnSignal, uint32 wait_event_info); extern void KeepFileRestoredFromArchive(const char *path, const char *xlogfname); diff --git a/src/include/meson.build b/src/include/meson.build index a28f115d86..3d30cf7972 100644 --- a/src/include/meson.build +++ b/src/include/meson.build @@ -150,6 +150,7 @@ header_subdirs = [ 'postmaster', 'regex', 'replication', + 'restore', 'rewrite', 'statistics', 'storage', diff --git a/src/include/postmaster/startup.h b/src/include/postmaster/startup.h index dde7ebde88..6ba1e48c02 100644 --- a/src/include/postmaster/startup.h +++ b/src/include/postmaster/startup.h @@ -26,6 +26,7 @@ extern PGDLLIMPORT int log_startup_progress_interval; extern void HandleStartupProcInterrupts(void); +extern void HandleStartupProcShutdownRequests(void); extern void StartupProcessMain(char *startup_data, size_t startup_data_len) pg_attribute_noreturn(); extern void PreRestoreCommand(void); extern void PostRestoreCommand(void); diff --git a/src/include/restore/shell_restore.h b/src/include/restore/shell_restore.h new file mode 100644 index 0000000000..28b5b7bc92 --- /dev/null +++ b/src/include/restore/shell_restore.h @@ -0,0 +1,26 @@ +/*------------------------------------------------------------------------- + * + * shell_restore.h + * Exports for restoring via shell. + * + * Copyright (c) 2024, PostgreSQL Global Development Group + * + * src/include/restore/shell_restore.h + * + *------------------------------------------------------------------------- + */ +#ifndef _SHELL_RESTORE_H +#define _SHELL_RESTORE_H + +extern bool shell_restore_file_configured(void); +extern bool shell_restore_wal_segment(const char *file, const char *path, + const char *lastRestartPointFileName); +extern bool shell_restore_timeline_history(const char *file, const char *path); + +extern bool shell_archive_cleanup_configured(void); +extern void shell_archive_cleanup(const char *lastRestartPointFileName); + +extern bool shell_recovery_end_configured(void); +extern void shell_recovery_end(const char *lastRestartPointFileName); + +#endif /* _SHELL_RESTORE_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 2b83c340fb..886a49de6f 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -136,6 +136,7 @@ ArchiveOpts ArchiveShutdownCB ArchiveStartupCB ArchiveStreamState +ArchiveType ArchiverOutput ArchiverStage ArrayAnalyzeExtraData -- 2.25.1
>From e8a18f516343f3a3eef66ca66ca6367cf4e24d75 Mon Sep 17 00:00:00 2001 From: Nathan Bossart <nathandboss...@gmail.com> Date: Wed, 15 Feb 2023 14:45:17 -0800 Subject: [PATCH v22 3/5] rename archive-modules.sgml to archive-and-restore-modules.sgml --- .../{archive-modules.sgml => archive-and-restore-modules.sgml} | 0 doc/src/sgml/filelist.sgml | 2 +- doc/src/sgml/postgres.sgml | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename doc/src/sgml/{archive-modules.sgml => archive-and-restore-modules.sgml} (100%) diff --git a/doc/src/sgml/archive-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml similarity index 100% rename from doc/src/sgml/archive-modules.sgml rename to doc/src/sgml/archive-and-restore-modules.sgml diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml index 38ec362d8f..be387bd7aa 100644 --- a/doc/src/sgml/filelist.sgml +++ b/doc/src/sgml/filelist.sgml @@ -101,7 +101,7 @@ <!ENTITY custom-scan SYSTEM "custom-scan.sgml"> <!ENTITY logicaldecoding SYSTEM "logicaldecoding.sgml"> <!ENTITY replication-origins SYSTEM "replication-origins.sgml"> -<!ENTITY archive-modules SYSTEM "archive-modules.sgml"> +<!ENTITY archive-and-restore-modules SYSTEM "archive-and-restore-modules.sgml"> <!ENTITY protocol SYSTEM "protocol.sgml"> <!ENTITY sources SYSTEM "sources.sgml"> <!ENTITY storage SYSTEM "storage.sgml"> diff --git a/doc/src/sgml/postgres.sgml b/doc/src/sgml/postgres.sgml index ec9f90e283..d668ad89f6 100644 --- a/doc/src/sgml/postgres.sgml +++ b/doc/src/sgml/postgres.sgml @@ -227,7 +227,7 @@ break is not needed in a wider output rendering. &bgworker; &logicaldecoding; &replication-origins; - &archive-modules; + &archive-and-restore-modules; </part> -- 2.25.1
>From 45f660e43aaef84f6d59fa4c4f975a8f6751ba2e Mon Sep 17 00:00:00 2001 From: Nathan Bossart <nathandboss...@gmail.com> Date: Wed, 15 Feb 2023 14:48:36 -0800 Subject: [PATCH v22 4/5] restructure archive modules docs in preparation for restore modules --- doc/src/sgml/archive-and-restore-modules.sgml | 242 +++++++++--------- 1 file changed, 128 insertions(+), 114 deletions(-) diff --git a/doc/src/sgml/archive-and-restore-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml index 10ec96eae9..6e368290b9 100644 --- a/doc/src/sgml/archive-and-restore-modules.sgml +++ b/doc/src/sgml/archive-and-restore-modules.sgml @@ -1,9 +1,9 @@ -<!-- doc/src/sgml/archive-modules.sgml --> +<!-- doc/src/sgml/archive-and-restore-modules.sgml --> -<chapter id="archive-modules"> - <title>Archive Modules</title> - <indexterm zone="archive-modules"> - <primary>Archive Modules</primary> +<chapter id="archive-and-restore-modules"> + <title>Archive and Restore Modules</title> + <indexterm zone="archive-and-restore-modules"> + <primary>Archive and Restore Modules</primary> </indexterm> <para> @@ -14,45 +14,52 @@ performant. </para> - <para> - When a custom <xref linkend="guc-archive-library"/> is configured, PostgreSQL - will submit completed WAL files to the module, and the server will avoid - recycling or removing these WAL files until the module indicates that the files - were successfully archived. It is ultimately up to the module to decide what - to do with each WAL file, but many recommendations are listed at - <xref linkend="backup-archiving-wal"/>. - </para> - - <para> - Archiving modules must at least consist of an initialization function (see - <xref linkend="archive-module-init"/>) and the required callbacks (see - <xref linkend="archive-module-callbacks"/>). However, archive modules are - also permitted to do much more (e.g., declare GUCs and register background - workers). - </para> - <para> The <filename>contrib/basic_archive</filename> module contains a working example, which demonstrates some useful techniques. </para> - <sect1 id="archive-module-init"> - <title>Initialization Functions</title> - <indexterm zone="archive-module-init"> - <primary>_PG_archive_module_init</primary> + <sect1 id="archive-modules"> + <title>Archive Modules</title> + <indexterm zone="archive-modules"> + <primary>Archive Modules</primary> </indexterm> + + <para> + When a custom <xref linkend="guc-archive-library"/> is configured, + PostgreSQL will submit completed WAL files to the module, and the server + will avoid recycling or removing these WAL files until the module indicates + that the files were successfully archived. It is ultimately up to the + module to decide what to do with each WAL file, but many recommendations are + listed at <xref linkend="backup-archiving-wal"/>. + </para> + <para> - An archive library is loaded by dynamically loading a shared library with the - <xref linkend="guc-archive-library"/>'s name as the library base name. The - normal library search path is used to locate the library. To provide the - required archive module callbacks and to indicate that the library is - actually an archive module, it needs to provide a function named - <function>_PG_archive_module_init</function>. The result of the function - must be a pointer to a struct of type - <structname>ArchiveModuleCallbacks</structname>, which contains everything - that the core code needs to know to make use of the archive module. The - return value needs to be of server lifetime, which is typically achieved by - defining it as a <literal>static const</literal> variable in global scope. + Archiving modules must at least consist of an initialization function (see + <xref linkend="archive-module-init"/>) and the required callbacks (see + <xref linkend="archive-module-callbacks"/>). However, archive modules are + also permitted to do much more (e.g., declare GUCs and register background + workers). + </para> + + <sect2 id="archive-module-init"> + <title>Initialization Functions</title> + <indexterm zone="archive-module-init"> + <primary>_PG_archive_module_init</primary> + </indexterm> + + <para> + An archive library is loaded by dynamically loading a shared library with + the <xref linkend="guc-archive-library"/>'s name as the library base name. + The normal library search path is used to locate the library. To provide + the required archive module callbacks and to indicate that the library is + actually an archive module, it needs to provide a function named + <function>_PG_archive_module_init</function>. The result of the function + must be a pointer to a struct of type + <structname>ArchiveModuleCallbacks</structname>, which contains everything + that the core code needs to know to make use of the archive module. The + return value needs to be of server lifetime, which is typically achieved by + defining it as a <literal>static const</literal> variable in global scope. <programlisting> typedef struct ArchiveModuleCallbacks @@ -65,112 +72,119 @@ typedef struct ArchiveModuleCallbacks typedef const ArchiveModuleCallbacks *(*ArchiveModuleInit) (void); </programlisting> - Only the <function>archive_file_cb</function> callback is required. The - others are optional. - </para> - </sect1> + Only the <function>archive_file_cb</function> callback is required. The + others are optional. + </para> + </sect2> - <sect1 id="archive-module-callbacks"> - <title>Archive Module Callbacks</title> - <para> - The archive callbacks define the actual archiving behavior of the module. - The server will call them as required to process each individual WAL file. - </para> + <sect2 id="archive-module-callbacks"> + <title>Archive Module Callbacks</title> - <sect2 id="archive-module-startup"> - <title>Startup Callback</title> <para> - The <function>startup_cb</function> callback is called shortly after the - module is loaded. This callback can be used to perform any additional - initialization required. If the archive module has any state, it can use - <structfield>state->private_data</structfield> to store it. + The archive callbacks define the actual archiving behavior of the module. + The server will call them as required to process each individual WAL file. + </para> + + <sect3 id="archive-module-startup"> + <title>Startup Callback</title> + + <para> + The <function>startup_cb</function> callback is called shortly after the + module is loaded. This callback can be used to perform any additional + initialization required. If the archive module has any state, it can use + <structfield>state->private_data</structfield> to store it. <programlisting> typedef void (*ArchiveStartupCB) (ArchiveModuleState *state); </programlisting> - </para> - </sect2> + </para> + </sect3> - <sect2 id="archive-module-check"> - <title>Check Callback</title> - <para> - The <function>check_configured_cb</function> callback is called to determine - whether the module is fully configured and ready to accept WAL files (e.g., - its configuration parameters are set to valid values). If no - <function>check_configured_cb</function> is defined, the server always - assumes the module is configured. + <sect3 id="archive-module-check"> + <title>Check Callback</title> + + <para> + The <function>check_configured_cb</function> callback is called to + determine whether the module is fully configured and ready to accept WAL + files (e.g., its configuration parameters are set to valid values). If no + <function>check_configured_cb</function> is defined, the server always + assumes the module is configured. <programlisting> typedef bool (*ArchiveCheckConfiguredCB) (ArchiveModuleState *state); </programlisting> - If <literal>true</literal> is returned, the server will proceed with - archiving the file by calling the <function>archive_file_cb</function> - callback. If <literal>false</literal> is returned, archiving will not - proceed, and the archiver will emit the following message to the server log: + If <literal>true</literal> is returned, the server will proceed with + archiving the file by calling the <function>archive_file_cb</function> + callback. If <literal>false</literal> is returned, archiving will not + proceed, and the archiver will emit the following message to the server + log: <screen> WARNING: archive_mode enabled, yet archiving is not configured </screen> - In the latter case, the server will periodically call this function, and - archiving will proceed only when it returns <literal>true</literal>. - </para> - - <note> - <para> - When returning <literal>false</literal>, it may be useful to append some - additional information to the generic warning message. To do that, provide - a message to the <function>arch_module_check_errdetail</function> macro - before returning <literal>false</literal>. Like - <function>errdetail()</function>, this macro accepts a format string - followed by an optional list of arguments. The resulting string will be - emitted as the <literal>DETAIL</literal> line of the warning message. + In the latter case, the server will periodically call this function, and + archiving will proceed only when it returns <literal>true</literal>. </para> - </note> - </sect2> - <sect2 id="archive-module-archive"> - <title>Archive Callback</title> - <para> - The <function>archive_file_cb</function> callback is called to archive a - single WAL file. + <note> + <para> + When returning <literal>false</literal>, it may be useful to append some + additional information to the generic warning message. To do that, + provide a message to the <function>arch_module_check_errdetail</function> + macro before returning <literal>false</literal>. Like + <function>errdetail()</function>, this macro accepts a format string + followed by an optional list of arguments. The resulting string will be + emitted as the <literal>DETAIL</literal> line of the warning message. + </para> + </note> + </sect3> + + <sect3 id="archive-module-archive"> + <title>Archive Callback</title> + + <para> + The <function>archive_file_cb</function> callback is called to archive a + single WAL file. <programlisting> typedef bool (*ArchiveFileCB) (ArchiveModuleState *state, const char *file, const char *path); </programlisting> - If <literal>true</literal> is returned, the server proceeds as if the file - was successfully archived, which may include recycling or removing the - original WAL file. If <literal>false</literal> is returned or an error is thrown, the server will - keep the original WAL file and retry archiving later. - <replaceable>file</replaceable> will contain just the file name of the WAL - file to archive, while <replaceable>path</replaceable> contains the full - path of the WAL file (including the file name). - </para> - - <note> - <para> - The <function>archive_file_cb</function> callback is called in a - short-lived memory context that will be reset between invocations. If you - need longer-lived storage, create a memory context in the module's - <function>startup_cb</function> callback. + If <literal>true</literal> is returned, the server proceeds as if the file + was successfully archived, which may include recycling or removing the + original WAL file. If <literal>false</literal> is returned or an error is + thrown, the server will keep the original WAL file and retry archiving + later. <replaceable>file</replaceable> will contain just the file name of + the WAL file to archive, while <replaceable>path</replaceable> contains the + full path of the WAL file (including the file name). </para> - </note> - </sect2> - <sect2 id="archive-module-shutdown"> - <title>Shutdown Callback</title> - <para> - The <function>shutdown_cb</function> callback is called when the archiver - process exits (e.g., after an error) or the value of - <xref linkend="guc-archive-library"/> changes. If no - <function>shutdown_cb</function> is defined, no special action is taken in - these situations. If the archive module has any state, this callback should - free it to avoid leaks. + <note> + <para> + The <function>archive_file_cb</function> callback is called in a + short-lived memory context that will be reset between invocations. If you + need longer-lived storage, create a memory context in the module's + <function>startup_cb</function> callback. + </para> + </note> + </sect3> + + <sect3 id="archive-module-shutdown"> + <title>Shutdown Callback</title> + + <para> + The <function>shutdown_cb</function> callback is called when the archiver + process exits (e.g., after an error) or the value of + <xref linkend="guc-archive-library"/> changes. If no + <function>shutdown_cb</function> is defined, no special action is taken in + these situations. If the archive module has any state, this callback + should free it to avoid leaks. <programlisting> typedef void (*ArchiveShutdownCB) (ArchiveModuleState *state); </programlisting> - </para> + </para> + </sect3> </sect2> </sect1> </chapter> -- 2.25.1
>From 4a0e021e7b5d5ce1536fc96ba91dd125bb22e12a Mon Sep 17 00:00:00 2001 From: Nathan Bossart <nathandboss...@gmail.com> Date: Wed, 15 Feb 2023 22:06:04 -0800 Subject: [PATCH v22 5/5] introduce restore_library --- contrib/basic_archive/Makefile | 2 + contrib/basic_archive/basic_archive.c | 153 +++++++- contrib/basic_archive/meson.build | 5 + contrib/basic_archive/t/001_restore.pl | 44 +++ doc/src/sgml/archive-and-restore-modules.sgml | 356 +++++++++++++++++- doc/src/sgml/backup.sgml | 40 +- doc/src/sgml/basic-archive.sgml | 31 +- doc/src/sgml/config.sgml | 53 ++- doc/src/sgml/high-availability.sgml | 18 +- src/backend/access/transam/xlog.c | 20 +- src/backend/access/transam/xlogarchive.c | 96 ++++- src/backend/access/transam/xlogrecovery.c | 14 +- src/backend/postmaster/checkpointer.c | 54 +++ src/backend/postmaster/startup.c | 44 ++- src/backend/restore/shell_restore.c | 85 ++++- src/backend/utils/misc/guc_tables.c | 11 + src/backend/utils/misc/postgresql.conf.sample | 1 + src/include/restore/restore_module.h | 143 +++++++ src/include/restore/shell_restore.h | 16 +- src/tools/pgindent/typedefs.list | 2 + 20 files changed, 1104 insertions(+), 84 deletions(-) create mode 100644 contrib/basic_archive/t/001_restore.pl create mode 100644 src/include/restore/restore_module.h diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile index 100ed81f12..e61f7d676f 100644 --- a/contrib/basic_archive/Makefile +++ b/contrib/basic_archive/Makefile @@ -10,6 +10,8 @@ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.c # typical installcheck users do not have (e.g. buildfarm clients). NO_INSTALLCHECK = 1 +TAP_TESTS = 1 + ifdef USE_PGXS PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c index 028cf51c25..e84bad27d2 100644 --- a/contrib/basic_archive/basic_archive.c +++ b/contrib/basic_archive/basic_archive.c @@ -17,6 +17,11 @@ * a file is successfully archived and then the system crashes before * a durable record of the success has been made. * + * This file also demonstrates a basic restore library implementation that + * is roughly equivalent to the following shell command: + * + * cp /path/to/archivedir/%f %p + * * Copyright (c) 2022-2024, PostgreSQL Global Development Group * * IDENTIFICATION @@ -33,6 +38,7 @@ #include "archive/archive_module.h" #include "common/int.h" #include "miscadmin.h" +#include "restore/restore_module.h" #include "storage/copydir.h" #include "storage/fd.h" #include "utils/guc.h" @@ -46,6 +52,16 @@ static bool basic_archive_configured(ArchiveModuleState *state); static bool basic_archive_file(ArchiveModuleState *state, const char *file, const char *path); static bool check_archive_directory(char **newval, void **extra, GucSource source); static bool compare_files(const char *file1, const char *file2); +static bool basic_restore_configured(RestoreModuleState *state); +static bool basic_restore_wal_segment(RestoreModuleState *state, + const char *file, const char *path, + const char *lastRestartPointFileName); +static bool basic_restore_timeline_history(RestoreModuleState *state, + const char *file, const char *path); +static bool basic_restore_file(const char *file, const char *path); +static bool basic_restore_timeline_history_exists(RestoreModuleState *state, + const char *file, + const char *path); static const ArchiveModuleCallbacks basic_archive_callbacks = { .startup_cb = NULL, @@ -54,6 +70,21 @@ static const ArchiveModuleCallbacks basic_archive_callbacks = { .shutdown_cb = NULL }; +static const RestoreModuleCallbacks basic_restore_callbacks = { + .startup_cb = NULL, + .restore_wal_segment_configured_cb = basic_restore_configured, + .restore_wal_segment_cb = basic_restore_wal_segment, + .restore_timeline_history_configured_cb = basic_restore_configured, + .restore_timeline_history_cb = basic_restore_timeline_history, + .timeline_history_exists_configured_cb = basic_restore_configured, + .timeline_history_exists_cb = basic_restore_timeline_history_exists, + .archive_cleanup_configured_cb = NULL, + .archive_cleanup_cb = NULL, + .recovery_end_configured_cb = NULL, + .recovery_end_cb = NULL, + .shutdown_cb = NULL +}; + /* * _PG_init * @@ -63,7 +94,7 @@ void _PG_init(void) { DefineCustomStringVariable("basic_archive.archive_directory", - gettext_noop("Archive file destination directory."), + gettext_noop("Archive file source/destination directory."), NULL, &archive_directory, "", @@ -85,6 +116,17 @@ _PG_archive_module_init(void) return &basic_archive_callbacks; } +/* + * _PG_restore_module_init + * + * Returns the module's restore callbacks. + */ +const RestoreModuleCallbacks * +_PG_restore_module_init(void) +{ + return &basic_restore_callbacks; +} + /* * check_archive_directory * @@ -307,3 +349,112 @@ compare_files(const char *file1, const char *file2) return ret; } + +/* + * basic_restore_configured + * + * Checks that archive_directory is not blank. + */ +static bool +basic_restore_configured(RestoreModuleState *state) +{ + return archive_directory != NULL && archive_directory[0] != '\0'; +} + +/* + * basic_restore_wal_segment + * + * Retrieves one archived WAL segment. + */ +static bool +basic_restore_wal_segment(RestoreModuleState *state, + const char *file, const char *path, + const char *lastRestartPointFileName) +{ + return basic_restore_file(file, path); +} + +/* + * basic_restore_timeline_history + * + * Retrieves one timeline history file. + */ +static bool +basic_restore_timeline_history(RestoreModuleState *state, + const char *file, const char *path) +{ + return basic_restore_file(file, path); +} + +/* + * basic_restore_file + * + * Retrieves one file. + */ +static bool +basic_restore_file(const char *file, const char *path) +{ + char archive_path[MAXPGPATH]; + struct stat st; + + ereport(DEBUG1, + (errmsg("restoring \"%s\" via basic_archive", + file))); + + /* + * If the file doesn't exist, return false to indicate that there are no + * more files to restore. + */ + snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file); + if (stat(archive_path, &st) != 0) + { + int elevel = (errno == ENOENT) ? DEBUG1 : ERROR; + + ereport(elevel, + (errcode_for_file_access(), + errmsg("could not stat file \"%s\": %m", + archive_path))); + return false; + } + + copy_file(archive_path, path); + + ereport(DEBUG1, + (errmsg("restored \"%s\" via basic_archive", + file))); + return true; +} + +/* + * basic_restore_timeline_history_exists + * + * Check whether a timeline history file is present in the archive directory. + */ +static bool +basic_restore_timeline_history_exists(RestoreModuleState *state, + const char *file, const char *path) +{ + char archive_path[MAXPGPATH]; + struct stat st; + + ereport(DEBUG1, + (errmsg("checking existence of \"%s\" via basic_archive", + file))); + + snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file); + if (stat(archive_path, &st) != 0) + { + int elevel = (errno == ENOENT) ? DEBUG1 : ERROR; + + ereport(elevel, + (errcode_for_file_access(), + errmsg("could not stat file \"%s\": %m", + archive_path))); + return false; + } + + ereport(DEBUG1, + (errmsg("verified existence of \"%s\" via basic_archive", + file))); + return true; +} diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build index cc2f62bf36..8e36fa7ed6 100644 --- a/contrib/basic_archive/meson.build +++ b/contrib/basic_archive/meson.build @@ -31,4 +31,9 @@ tests += { # typical installcheck users do not have (e.g. buildfarm clients). 'runningcheck': false, }, + 'tap': { + 'tests': [ + 't/001_restore.pl', + ], + }, } diff --git a/contrib/basic_archive/t/001_restore.pl b/contrib/basic_archive/t/001_restore.pl new file mode 100644 index 0000000000..6c01f736cf --- /dev/null +++ b/contrib/basic_archive/t/001_restore.pl @@ -0,0 +1,44 @@ + +# Copyright (c) 2024, PostgreSQL Global Development Group + +use strict; +use warnings; +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +# start a node +my $node = PostgreSQL::Test::Cluster->new('node'); +$node->init(has_archiving => 1, allows_streaming => 1); +my $archive_dir = $node->archive_dir; +$archive_dir =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os; +$node->append_conf('postgresql.conf', "archive_command = ''"); +$node->append_conf('postgresql.conf', "archive_library = 'basic_archive'"); +$node->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'"); +$node->start; + +# backup the node +my $backup = 'backup'; +$node->backup($backup); + +# generate some new WAL files +$node->safe_psql('postgres', "CREATE TABLE test (a INT);"); +$node->safe_psql('postgres', "SELECT pg_switch_wal();"); +$node->safe_psql('postgres', "INSERT INTO test VALUES (1);"); + +# shut down the node (this should archive all WAL files) +$node->stop; + +# restore from the backup +my $restore = PostgreSQL::Test::Cluster->new('restore'); +$restore->init_from_backup($node, $backup, has_restoring => 1, standby => 0); +$restore->append_conf('postgresql.conf', "restore_command = ''"); +$restore->append_conf('postgresql.conf', "restore_library = 'basic_archive'"); +$restore->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'"); +$restore->start; + +# ensure post-backup WAL is replayed +my $result = $restore->poll_query_until("postgres", "SELECT count(*) = 1 FROM test;"); +is($result, "1", "check restore content"); + +done_testing(); diff --git a/doc/src/sgml/archive-and-restore-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml index 6e368290b9..bcfa88dee0 100644 --- a/doc/src/sgml/archive-and-restore-modules.sgml +++ b/doc/src/sgml/archive-and-restore-modules.sgml @@ -8,15 +8,17 @@ <para> PostgreSQL provides infrastructure to create custom modules for continuous - archiving (see <xref linkend="continuous-archiving"/>). While archiving via - a shell command (i.e., <xref linkend="guc-archive-command"/>) is much - simpler, a custom archive module will often be considerably more robust and - performant. + archiving and recovery (see <xref linkend="continuous-archiving"/>). While a + shell command (i.e., <xref linkend="guc-archive-command"/>, + <xref linkend="guc-restore-command"/>) is much simpler, a custom module will + often be considerably more robust and performant. </para> <para> The <filename>contrib/basic_archive</filename> module contains a working - example, which demonstrates some useful techniques. + example, which demonstrates some useful techniques. As demonstrated in + <filename>basic_archive</filename> a single module may serve as both an + archive module and a restore module. </para> <sect1 id="archive-modules"> @@ -187,4 +189,348 @@ typedef void (*ArchiveShutdownCB) (ArchiveModuleState *state); </sect3> </sect2> </sect1> + + <sect1 id="restore-modules"> + <title>Restore Modules</title> + <indexterm zone="restore-modules"> + <primary>Restore Modules</primary> + </indexterm> + + <para> + When a custom <xref linkend="guc-restore-library"/> is configured, + PostgreSQL will use the module for restore actions. It is + ultimately up to the module to decide how to accomplish each task, + but some recommendations are listed at + <xref linkend="backup-pitr-recovery"/>. + </para> + + <para> + Restore modules must at least consist of an initialization function + (see <xref linkend="restore-module-init"/>) and the required callbacks (see + <xref linkend="restore-module-callbacks"/>). However, restore modules are + also permitted to do much more (e.g., declare GUCs and register background + workers). + </para> + + <sect2 id="restore-module-init"> + <title>Initialization Functions</title> + <indexterm zone="restore-module-init"> + <primary>_PG_restore_module_init</primary> + </indexterm> + + <para> + An restore library is loaded by dynamically loading a shared library with + the <xref linkend="guc-restore-library"/>'s name as the library base name. + The normal library search path is used to locate the library. To provide + the required restore module callbacks and to indicate that the library is + actually a restore module, it needs to provide a function named + <function>_PG_restore_module_init</function>. The result of the function + must be a pointer to a struct of type + <structname>RestoreModuleCallbacks</structname>, which contains everything + that the core code needs to know how to make use of the restore module. + The return value needs to be of server lifetime, which is typically + achieved by defining it as a <literal>static const</literal> variable in + global scope. + +<programlisting> +typedef struct RestoreModuleCallbacks +{ + RestoreStartupCB startup_cb; + RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb; + RestoreWalSegmentCB restore_wal_segment_cb; + RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb; + RestoreTimelineHistoryCB restore_timeline_history_cb; + TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb; + TimelineHistoryExistsCB timeline_history_exists_cb; + ArchiveCleanupConfiguredCB archive_cleanup_configured_cb; + ArchiveCleanupCB archive_cleanup_cb; + RecoveryEndConfiguredCB recovery_end_configured_cb; + RecoveryEndCB recovery_end_cb; + RestoreShutdownCB shutdown_cb; +} RestoreModuleCallbacks; +typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void); +</programlisting> + + The <function>restore_wal_segment_configured_cb</function>, + <function>restore_wal_segment_cb</function>, + <function>restore_timeline_history_configured_cb</function>, + <function>restore_timeline_history_cb</function>, + <function>timeline_history_exists_configured_cb</function>, and + <function>timeline_history_exists_cb</function> callbacks are required for + archive recovery but optional for streaming replication. The others are + always optional. + </para> + </sect2> + + <sect2 id="restore-module-callbacks"> + <title>Restore Module Callbacks</title> + + <para> + The restore callbacks define the actual behavior of the module. The server + will call them as required to execute restore actions. + </para> + + <sect3 id="restore-module-startup"> + <title>Startup Callback</title> + + <para> + The <function>startup_cb</function> callback is called shortly after the + module is loaded. This callback can be used to perform any additional + initialization required. If the restore module has state, it can use + <structfield>state->private_data</structfield> to store it. + +<programlisting> +typedef void (*RestoreStartupCB) (RestoreModuleState *state); +</programlisting> + </para> + </sect3> + + <sect3 id="restore-module-restore-wal-segment-configured"> + <title>Restore WAL Segment Configured Callback</title> + <para> + The <function>restore_wal_segment_configured_cb</function> callback is + called to determine whether the module is fully configured and ready to + accept requests to retrieve WAL files (e.g., its configuration parameters + are set to valid values). If no + <function>restore_wal_segment_configured_cb</function> is defined, the + server always assumes the module is not configured to retrieve WAL files. + +<programlisting> +typedef bool (*RestoreWalSegmentConfiguredCB) (RestoreModuleState *state); +</programlisting> + + If <literal>true</literal> is returned, the server will retrieve WAL files + by calling the <function>restore_wal_segment_cb</function> callback. If + <literal>false</literal> is returned, the server will not use the + <function>restore_wal_segment_cb</function> callback to retrieve WAL + files. + </para> + </sect3> + + <sect3 id="restore-module-restore-wal-segment"> + <title>Restore WAL Segment Callback</title> + <para> + The <function>restore_wal_segment_cb</function> callback is called to + retrieve a single archived segment of the WAL file series for archive + recovery or streaming replication. + +<programlisting> +typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state, const char *file, const char *path, const char *lastRestartPointFileName); +</programlisting> + + This callback must return <literal>true</literal> only if the file was + successfully retrieved. If the file is not available in the archives, the + callback must return <literal>false</literal>. + <replaceable>file</replaceable> will contain just the file name + of the WAL file to retrieve, while <replaceable>path</replaceable> + contains the destination's relative path (including the file name). + <replaceable>lastRestartPointFileName</replaceable> will contain the name + of the file containing the last valid restart point. That is the earliest + file that must be kept to allow a restore to be restartable, so this + information can be used to truncate the archive to just the minimum + required to support restarting from the current restore. + <replaceable>lastRestartPointFileName</replaceable> is typically only used + by warm-standby configurations (see <xref linkend="warm-standby"/>). Note + that if multiple standby servers are restoring from the same archive + directory, you will need to ensure that you do not delete WAL files until + they are no longer needed by any of the servers. + </para> + </sect3> + + <sect3 id="restore-module-restore-timeline-history-configured"> + <title>Restore Timeline History Configured Callback</title> + <para> + The <function>restore_timeline_history_configured_cb</function> callback + is called to determine whether the module is fully configured and ready to + accept requests to retrieve timeline history files (e.g., its + configuration parameters are set to valid values). If no + <function>restore_timeline_history_configured_cb</function> is defined, + the server always assumes the module is not configured to retrieve + timeline history files. + +<programlisting> +typedef bool (*RestoreTimelineHistoryConfiguredCB) (RestoreModuleState *state); +</programlisting> + + If <literal>true</literal> is returned, the server will retrieve timeline + history files by calling the + <function>restore_timeline_history_cb</function> callback. If + <literal>false</literal> is returned, the server will not use the + <function>restore_timeline_history_cb</function> callback to retrieve + timeline history files. + </para> + </sect3> + + <sect3 id="restore-module-restore-timeline-history"> + <title>Restore Timeline History Callback</title> + <para> + The <function>restore_timeline_history_cb</function> callback is called to + retrieve a single archived timeline history file for archive recovery or + streaming replication. + +<programlisting> +typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state, const char *file, const char *path); +</programlisting> + + This callback must return <literal>true</literal> only if the file was + successfully retrieved. If the file is not available in the archives, the + callback must return <literal>false</literal>. + <replaceable>file</replaceable> will contain just the file name + of the WAL file to retrieve, while <replaceable>path</replaceable> + contains the destination's relative path (including the file name). + </para> + </sect3> + + <sect3 id="restore-module-timeline-history-exists-configured"> + <title>Timeline History Exists Configured Callback</title> + <para> + The <function>timeline_history_exists_configured_cb</function> callback is + called to determine whether the module is fully configured and ready to + accept requests to determine whether a timeline history file exists in the + archives (e.g., its configuration parameters are set to valid values). If + no <function>timeline_history_exists_configured_cb</function> is defined, + the server always assumes the module is not configured to check whether + certain timeline history files exist. + +<programlisting> +typedef bool (*TimelineHistoryConfiguredCB) (RestoreModuleState *state); +</programlisting> + + If <literal>true</literal> is returned, the server will check whether + certain timeline history files are present in the archives by calling the + <function>timeline_history_exists_cb</function> callback. If + <literal>false</literal> is returned, the server will not use the + <function>timeline_history_exists_cb</function> callback to check if + timeline history files exist in the archives. + </para> + </sect3> + + <sect3 id="restore-module-timeline-history-exists"> + <title>Timeline History Exists Callback</title> + <para> + The <function>timeline_history_exists_cb</function> callback is called to + check whether a timeline history file is present in the archives. + +<programlisting> +typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state, const char *file, const char *path); +</programlisting> + + This callback must return <literal>true</literal> only if the file is + present in the archives. If the file is not available in the archives, + the callback must return <literal>false</literal>. + <replaceable>file</replaceable> will contain just the file name + of the timeline history file. + </para> + + <para> + Some restore modules might not have a dedicated way to determine whether a + timeline history file exists. For example, + <varname>restore_command</varname> only tells the server how to retrieve + files. In this case, the <function>timeline_history_exists_cb</function> + callback should copy the file to <replaceable>path</replaceable>, which + contains the destination's relative path (including the file name), and + the restore module should set + <structfield>state->timeline_history_exists_cb_copies</structfield> to + <literal>true</literal>. It is recommended to set this flag in the + module's <function>startup_cb</function> callback. This flag tells the + server that it should verify that the file was successfully retrieved + after invoking this callback. + </para> + </sect3> + + <sect3 id="restore-module-archive-cleanup-configured"> + <title>Archive Cleanup Configured Callback</title> + <para> + The <function>archive_cleanup_configured_cb</function> callback is called + to determine whether the module is fully configured and ready to accept + requests to remove old WAL files from the archives (e.g., its + configuration parameters are set to valid values). If no + <function>archive_cleanup_configured_cb</function> is defined, the server + always assumes the module is not configured to remove old WAL files. + +<programlisting> +typedef bool (*ArchiveCleanupConfiguredCB) (RestoreModuleState *state); +</programlisting> + + If <literal>true</literal> is returned, the server will remove old WAL + files by calling the <function>archive_cleanup_cb</function> callback. If + <literal>false</literal> is returned, the server will not use the + <function>archive_cleanup_cb</function> callback to remove old WAL files. + </para> + </sect3> + + <sect3 id="restore-module-archive-cleanup"> + <title>Archive Cleanup Callback</title> + <para> + The <function>archive_cleanup_cb</function> callback is called at every + restart point by the checkpointer process and is intended to provide a + mechanism for cleaning up old archived WAL files that are no longer + needed by the standby server. + +<programlisting> +typedef void (*ArchiveCleanupCB) (RestoreModuleState *state, const char *lastRestartPointFileName); +</programlisting> + + <replaceable>lastRestartPointFileName</replaceable> will contain the + name of the file that includes the last valid restart point, like in + <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>. + </para> + </sect3> + + <sect3 id="restore-module-recovery-end-configured"> + <title>Recovery End Configured Callback</title> + <para> + The <function>recovery_end_configured_cb</function> callback is called to + determine whether the module is fully configured and ready to execute its + <function>recovery_end_cb</function> callback once at the end of recovery + (e.g., its configuration parameters are set to valid values). If no + <function>recovery_end_configured_cb</function> is defined, the server + always assumes the module is not configured to execute its + <function>recovery_end_cb</function> callback. + +<programlisting> +typedef bool (*RecoveryEndConfiguredCB) (RestoreModuleState *state); +</programlisting> + + If <literal>true</literal> is returned, the server will execute the + <function>recovery_end_cb</function> callback once at the end of recovery. + If <literal>false</literal> is returned, the server will not use the + <function>recovery_end_cb</function> callback at the end of recovery. + </para> + </sect3> + + <sect3 id="restore-module-recovery-end"> + <title>Recovery End Callback</title> + <para> + The <function>recovery_end_cb</function> callback is called once at the + end of recovery and is intended to provide a mechanism for cleanup + following the end of replication or recovery. + +<programlisting> +typedef void (*RecoveryEndCB) (RestoreModuleState *state, const char *lastRestartPointFileName); +</programlisting> + + <replaceable>lastRestartPointFileName</replaceable> will contain the name + of the file containing the last valid restart point, like in + <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>. + </para> + </sect3> + + <sect3 id="restore-module-shutdown"> + <title>Shutdown Callback</title> + <para> + The <function>shutdown_cb</function> callback is called when a process + that has loaded the restore module exits (e.g., after an error) or the + value of <xref linkend="guc-restore-library"/> changes. If no + <function>shutdown_cb</function> is defined, no special action is taken in + these situations. If the restore module has state, this callback should + free it to avoid leaks. + +<programlisting> +typedef void (*RestoreShutdownCB) (RestoreModuleState *state); + </programlisting> + </para> + </sect3> + </sect2> + </sect1> </chapter> diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml index 91da3c26ba..4469b45f60 100644 --- a/doc/src/sgml/backup.sgml +++ b/doc/src/sgml/backup.sgml @@ -1265,9 +1265,26 @@ SELECT * FROM pg_backup_stop(wait_for_archive => true); <para> The key part of all this is to set up a recovery configuration that describes how you want to recover and how far the recovery should - run. The one thing that you absolutely must specify is the <varname>restore_command</varname>, + run. The one thing that you absolutely must specify is either + <varname>restore_command</varname> or a <varname>restore_library</varname>, which tells <productname>PostgreSQL</productname> how to retrieve archived - WAL file segments. Like the <varname>archive_command</varname>, this is + WAL file segments. + </para> + + <para> + Like the <varname>archive_library</varname> parameter, + <varname>restore_library</varname> is a shared library. Since such + libraries are written in <literal>C</literal>, creating your own may + require considerably more effort than writing a shell command. However, + restore modules can be more performance than restoring via shell, and they + will have access to many useful server resources. For more information + about creating a <varname>restore_library</varname>, see + <xref linkend="restore-modules"/>. + </para> + + <para> + Like the <varname>archive_command</varname>, + <varname>restore_command</varname> is a shell command string. It can contain <literal>%f</literal>, which is replaced by the name of the desired WAL file, and <literal>%p</literal>, which is replaced by the path name to copy the WAL file to. @@ -1286,14 +1303,20 @@ restore_command = 'cp /mnt/server/archivedir/%f %p' </para> <para> - It is important that the command return nonzero exit status on failure. - The command <emphasis>will</emphasis> be called requesting files that are not - present in the archive; it must return nonzero when so asked. This is not - an error condition. An exception is that if the command was terminated by + It is important that the <varname>restore_command</varname> return nonzero + exit status on failure, or, if you are using a + <varname>restore_library</varname>, that the restore callbacks return + <literal>false</literal> on failure. The command or library + <emphasis>will</emphasis> be called requesting files that are not + present in the archive; it must fail when so asked. This is not + an error condition. An exception is that if the + <varname>restore_command</varname> was terminated by a signal (other than <systemitem>SIGTERM</systemitem>, which is used as part of a database server shutdown) or an error by the shell (such as command not found), then recovery will abort and the server will not start - up. + up. Likewise, if the restore callbacks provided by the + <varname>restore_library</varname> emit an <literal>ERROR</literal> or + <literal>FATAL</literal>, recovery will abort and the server won't start. </para> <para> @@ -1317,7 +1340,8 @@ restore_command = 'cp /mnt/server/archivedir/%f %p' close as possible given the available WAL segments). Therefore, a normal recovery will end with a <quote>file not found</quote> message, the exact text of the error message depending upon your choice of - <varname>restore_command</varname>. You may also see an error message + <varname>restore_command</varname> or <varname>restore_library</varname>. + You may also see an error message at the start of recovery for a file named something like <filename>00000001.history</filename>. This is also normal and does not indicate a problem in simple recovery situations; see diff --git a/doc/src/sgml/basic-archive.sgml b/doc/src/sgml/basic-archive.sgml index b4d43ced20..f3a5a502bd 100644 --- a/doc/src/sgml/basic-archive.sgml +++ b/doc/src/sgml/basic-archive.sgml @@ -8,17 +8,20 @@ </indexterm> <para> - <filename>basic_archive</filename> is an example of an archive module. This - module copies completed WAL segment files to the specified directory. This - may not be especially useful, but it can serve as a starting point for - developing your own archive module. For more information about archive - modules, see <xref linkend="archive-modules"/>. + <filename>basic_archive</filename> is an example of an archive and restore + module. This module copies completed WAL segment files to or from the + specified directory. This may not be especially useful, but it can serve as + a starting point for developing your own archive and restore modules. For + more information about archive and restore modules, see\ + <xref linkend="archive-and-restore-modules"/>. </para> <para> - In order to function, this module must be loaded via + For use as an archive module, this module must be loaded via <xref linkend="guc-archive-library"/>, and <xref linkend="guc-archive-mode"/> - must be enabled. + must be enabled. For use as a restore module, this module must be loaded + via <xref linkend="guc-restore-library"/>, and recovery must be enabled (see + <xref linkend="runtime-config-wal-archive-recovery"/>). </para> <sect2 id="basic-archive-configuration-parameters"> @@ -34,11 +37,12 @@ </term> <listitem> <para> - The directory where the server should copy WAL segment files. This - directory must already exist. The default is an empty string, which - effectively halts WAL archiving, but if <xref linkend="guc-archive-mode"/> - is enabled, the server will accumulate WAL segment files in the - expectation that a value will soon be provided. + The directory where the server should copy WAL segment files to or from. + This directory must already exist. The default is an empty string, + which, when used for archiving, effectively halts WAL archival, but if + <xref linkend="guc-archive-mode"/> is enabled, the server will accumulate + WAL segment files in the expectation that a value will soon be provided. + When an empty string is used for recovery, restore will fail. </para> </listitem> </varlistentry> @@ -46,7 +50,7 @@ <para> These parameters must be set in <filename>postgresql.conf</filename>. - Typical usage might be: + Typical usage as an archive module might be: </para> <programlisting> @@ -61,6 +65,7 @@ basic_archive.archive_directory = '/path/to/archive/directory' <title>Notes</title> <para> + When <filename>basic_archive</filename> is used as an archive module, Server crashes may leave temporary files with the prefix <filename>archtemp</filename> in the archive directory. It is recommended to delete such files before restarting the server after a crash. It is safe to diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index e93208b2e6..6742b56a4f 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -3909,7 +3909,8 @@ include_dir 'conf.d' recovery when the end of archived WAL is reached, but will keep trying to continue recovery by connecting to the sending server as specified by the <varname>primary_conninfo</varname> setting and/or by fetching new WAL - segments using <varname>restore_command</varname>. For this mode, the + segments using <varname>restore_command</varname> or + <varname>restore_library</varname>. For this mode, the parameters from this section and <xref linkend="runtime-config-replication-standby"/> are of interest. Parameters from <xref linkend="runtime-config-wal-recovery-target"/> will @@ -3937,7 +3938,8 @@ include_dir 'conf.d' <listitem> <para> The local shell command to execute to retrieve an archived segment of - the WAL file series. This parameter is required for archive recovery, + the WAL file series. Either <varname>restore_command</varname> or + <xref linkend="guc-restore-library"/> is required for archive recovery, but optional for streaming replication. Any <literal>%f</literal> in the string is replaced by the name of the file to retrieve from the archive, @@ -3972,7 +3974,42 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows <para> This parameter can only be set in the <filename>postgresql.conf</filename> - file or on the server command line. + file or on the server command line. It is only used if + <varname>restore_library</varname> is set to an empty string. If both + <varname>restore_command</varname> and + <varname>restore_library</varname> are set, an error will be raised. + </para> + </listitem> + </varlistentry> + + <varlistentry id="guc-restore-library" xreflabel="restore_library"> + <term><varname>restore_library</varname> (<type>string</type>) + <indexterm> + <primary><varname>restore_library</varname> configuration parameter</primary> + </indexterm> + </term> + <listitem> + <para> + The library to use for restore actions, including retrieving archived + files and executing tasks at restartpoints and at recovery end. Either + <xref linkend="guc-restore-command"/> or + <varname>restore_library</varname> is required for archive recovery, + but optional for streaming replication. If this parameter is set to an + empty string (the default), restoring via shell is enabled, and + <varname>restore_command</varname>, + <varname>archive_cleanup_command</varname>, and + <varname>recovery_end_command</varname> are used. If both + <varname>restore_library</varname> and any of + <varname>restore_command</varname>, + <varname>archive_cleanup_command</varname>, or + <varname>recovery_end_command</varname> are set, an error will be + raised. Otherwise, the specified shared library is used for restoring. + For more information, see <xref linkend="restore-modules"/>. + </para> + + <para> + This parameter can only be set in the + <filename>postgresql.conf</filename> file or on the server command line. </para> </listitem> </varlistentry> @@ -4017,7 +4054,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows </para> <para> This parameter can only be set in the <filename>postgresql.conf</filename> - file or on the server command line. + file or on the server command line. It is only used if + <varname>restore_library</varname> is set to an empty string. If both + <varname>archive_cleanup_command</varname> and + <varname>restore_library</varname> are set, an error will be raised. </para> </listitem> </varlistentry> @@ -4046,7 +4086,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows </para> <para> This parameter can only be set in the <filename>postgresql.conf</filename> - file or on the server command line. + file or on the server command line. It is only used if + <varname>restore_library</varname> is set to an empty string. If both + <varname>recovery_end_command</varname> and + <varname>restore_library</varname> are set, an error will be raised. </para> </listitem> </varlistentry> diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml index b48209fc2f..a86a9fe93a 100644 --- a/doc/src/sgml/high-availability.sgml +++ b/doc/src/sgml/high-availability.sgml @@ -627,7 +627,8 @@ protocol to make nodes agree on a serializable transactional order. <para> In standby mode, the server continuously applies WAL received from the primary server. The standby server can read WAL from a WAL archive - (see <xref linkend="guc-restore-command"/>) or directly from the primary + (see <xref linkend="guc-restore-command"/> and + <xref linkend="guc-restore-library"/>) or directly from the primary over a TCP connection (streaming replication). The standby server will also attempt to restore any WAL found in the standby cluster's <filename>pg_wal</filename> directory. That typically happens after a server @@ -638,8 +639,10 @@ protocol to make nodes agree on a serializable transactional order. <para> At startup, the standby begins by restoring all WAL available in the - archive location, calling <varname>restore_command</varname>. Once it + archive location, either by calling <varname>restore_command</varname> or + by executing the <varname>restore_library</varname>'s callbacks. Once it reaches the end of WAL available there and <varname>restore_command</varname> + or <varname>restore_library</varname> fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory. If that fails, and streaming replication has been configured, the standby tries to connect to the primary server and start streaming WAL @@ -698,7 +701,8 @@ protocol to make nodes agree on a serializable transactional order. server (see <xref linkend="backup-pitr-recovery"/>). Create a file <link linkend="file-standby-signal"><filename>standby.signal</filename></link><indexterm><primary>standby.signal</primary></indexterm> in the standby's cluster data - directory. Set <xref linkend="guc-restore-command"/> to a simple command to copy files from + directory. Set <xref linkend="guc-restore-command"/> or + <xref linkend="guc-restore-library"/> to copy files from the WAL archive. If you plan to have multiple standby servers for high availability purposes, make sure that <varname>recovery_target_timeline</varname> is set to <literal>latest</literal> (the default), to make the standby server follow the timeline change @@ -707,7 +711,8 @@ protocol to make nodes agree on a serializable transactional order. <note> <para> - <xref linkend="guc-restore-command"/> should return immediately + <xref linkend="guc-restore-command"/> and + <xref linkend="guc-restore-library"/> should return immediately if the file does not exist; the server will retry the command again if necessary. </para> @@ -731,8 +736,9 @@ protocol to make nodes agree on a serializable transactional order. <para> If you're using a WAL archive, its size can be minimized using the <xref - linkend="guc-archive-cleanup-command"/> parameter to remove files that are no - longer required by the standby server. + linkend="guc-archive-cleanup-command"/> parameter or the + <xref linkend="guc-restore-library"/>'s archive cleanup callbacks to remove + files that are no longer required by the standby server. The <application>pg_archivecleanup</application> utility is designed specifically to be used with <varname>archive_cleanup_command</varname> in typical single-standby configurations, see <xref linkend="pgarchivecleanup"/>. diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index 1eb6d57fe2..b7a7f58009 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -83,7 +83,7 @@ #include "replication/snapbuild.h" #include "replication/walreceiver.h" #include "replication/walsender.h" -#include "restore/shell_restore.h" +#include "restore/restore_module.h" #include "storage/bufmgr.h" #include "storage/fd.h" #include "storage/ipc.h" @@ -5258,18 +5258,19 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog, TimeLineID newTLI) { /* - * Execute the recovery_end_command, if any. + * Execute the recovery-end callback, if any. * - * The command is provided the archive file cutoff point for use during + * The callback is provided the archive file cutoff point for use during * log shipping replication. All files earlier than this point can be * deleted from the archive, though there is no requirement to do so. */ - if (shell_recovery_end_configured()) + if (recovery_end_configured()) { char lastRestartPointFname[MAXFNAMELEN]; GetOldestRestartPointFileName(lastRestartPointFname); - shell_recovery_end(lastRestartPointFname); + RestoreCallbacks->recovery_end_cb(restore_module_state, + lastRestartPointFname); } /* @@ -7760,18 +7761,19 @@ CreateRestartPoint(int flags) timestamptz_to_str(xtime)) : 0)); /* - * Finally, execute archive_cleanup_command, if any. + * Finally, execute archive-cleanup callback, if any. * - * The command is provided the archive file cutoff point for use during + * The callback is provided the archive file cutoff point for use during * log shipping replication. All files earlier than this point can be * deleted from the archive, though there is no requirement to do so. */ - if (shell_archive_cleanup_configured()) + if (archive_cleanup_configured()) { char lastRestartPointFname[MAXFNAMELEN]; GetOldestRestartPointFileName(lastRestartPointFname); - shell_archive_cleanup(lastRestartPointFname); + RestoreCallbacks->archive_cleanup_cb(restore_module_state, + lastRestartPointFname); } return true; diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c index 5756f52124..3a05581495 100644 --- a/src/backend/access/transam/xlogarchive.c +++ b/src/backend/access/transam/xlogarchive.c @@ -23,14 +23,21 @@ #include "access/xlog_internal.h" #include "access/xlogarchive.h" #include "common/archive.h" +#include "fmgr.h" #include "miscadmin.h" #include "pgstat.h" #include "postmaster/pgarch.h" #include "postmaster/startup.h" #include "replication/walsender.h" +#include "restore/restore_module.h" #include "restore/shell_restore.h" #include "storage/fd.h" #include "storage/ipc.h" +#include "utils/memutils.h" + +char *restoreLibrary = ""; +const RestoreModuleCallbacks *RestoreCallbacks = NULL; +RestoreModuleState *restore_module_state; /* * Attempt to retrieve the specified file from off-line archival storage. @@ -74,15 +81,15 @@ RestoreArchivedFile(char *path, const char *xlogfname, switch (archive_type) { case ARCHIVE_TYPE_WAL_SEGMENT: - if (!shell_restore_file_configured()) + if (!restore_wal_segment_configured()) goto not_available; break; case ARCHIVE_TYPE_TIMELINE_HISTORY: - if (!shell_restore_file_configured()) + if (!restore_timeline_history_configured()) goto not_available; break; case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS: - if (!shell_restore_file_configured()) + if (!timeline_history_exists_configured()) goto not_available; break; } @@ -174,14 +181,17 @@ RestoreArchivedFile(char *path, const char *xlogfname, switch (archive_type) { case ARCHIVE_TYPE_WAL_SEGMENT: - ret = shell_restore_wal_segment(xlogfname, xlogpath, - lastRestartPointFname); + ret = RestoreCallbacks->restore_wal_segment_cb(restore_module_state, + xlogfname, xlogpath, + lastRestartPointFname); break; case ARCHIVE_TYPE_TIMELINE_HISTORY: - ret = shell_restore_timeline_history(xlogfname, xlogpath); + ret = RestoreCallbacks->restore_timeline_history_cb(restore_module_state, + xlogfname, xlogpath); break; case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS: - ret = shell_restore_timeline_history(xlogfname, xlogpath); + ret = RestoreCallbacks->timeline_history_exists_cb(restore_module_state, + xlogfname, xlogpath); break; } @@ -189,6 +199,16 @@ RestoreArchivedFile(char *path, const char *xlogfname, if (ret) { + /* + * Some restore functions might not copy the file (e.g., checking + * whether a timeline history file exists), but they can set a flag to + * tell us if they do. We only need to verify file existence if this + * flag is enabled. + */ + if (archive_type == ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS && + !restore_module_state->timeline_history_exists_cb_copies) + return true; + /* * command apparently succeeded, but let's make sure the file is * really there now and has the correct size. @@ -630,3 +650,65 @@ XLogArchiveCleanup(const char *xlog) unlink(archiveStatusPath); /* should we complain about failure? */ } + +/* + * Loads all the restore callbacks into our global RestoreCallbacks. The + * caller is responsible for validating the combination of library/command + * parameters that are set (e.g., restore_command and restore_library cannot + * both be set). + */ +void +LoadRestoreCallbacks(void) +{ + RestoreModuleInit init; + + /* + * If the shell command is enabled, use our special initialization + * function. Otherwise, load the library and call its + * _PG_restore_module_init(). + */ + if (restoreLibrary[0] == '\0') + init = shell_restore_init; + else + init = (RestoreModuleInit) + load_external_function(restoreLibrary, "_PG_restore_module_init", + false, NULL); + + if (init == NULL) + ereport(ERROR, + (errmsg("restore modules have to define the symbol " + "_PG_restore_module_init"))); + + RestoreCallbacks = (*init) (); + + /* restore state should be freed before calling this function */ + Assert(restore_module_state == NULL); + restore_module_state = (RestoreModuleState *) + MemoryContextAllocZero(TopMemoryContext, + sizeof(RestoreModuleState)); + + if (RestoreCallbacks->startup_cb != NULL) + RestoreCallbacks->startup_cb(restore_module_state); +} + +/* + * Call the shutdown callback of the loaded restore module, if defined. Also, + * free the restore module state if it was allocated. + * + * Processes that load restore libraries should register this via + * before_shmem_exit(). + */ +void +call_restore_module_shutdown_cb(int code, Datum arg) +{ + if (RestoreCallbacks != NULL && + RestoreCallbacks->shutdown_cb != NULL && + restore_module_state != NULL) + RestoreCallbacks->shutdown_cb(restore_module_state); + + if (restore_module_state != NULL) + { + pfree(restore_module_state); + restore_module_state = NULL; + } +} diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c index 07213b1897..1bf0d0ea11 100644 --- a/src/backend/access/transam/xlogrecovery.c +++ b/src/backend/access/transam/xlogrecovery.c @@ -51,6 +51,7 @@ #include "replication/slot.h" #include "replication/slotsync.h" #include "replication/walreceiver.h" +#include "restore/restore_module.h" #include "storage/fd.h" #include "storage/ipc.h" #include "storage/latch.h" @@ -1117,18 +1118,21 @@ validateRecoveryParameters(void) if (StandbyModeRequested) { if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) && - (recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0)) + (!restore_wal_segment_configured() || + !restore_timeline_history_configured() || + !timeline_history_exists_configured())) ereport(WARNING, - (errmsg("specified neither primary_conninfo nor restore_command"), + (errmsg("specified neither primary_conninfo nor restore_command nor a configured restore_library"), errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there."))); } else { - if (recoveryRestoreCommand == NULL || - strcmp(recoveryRestoreCommand, "") == 0) + if (!restore_wal_segment_configured() || + !restore_timeline_history_configured() || + !timeline_history_exists_configured()) ereport(FATAL, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("must specify restore_command when standby mode is not enabled"))); + errmsg("must specify restore_command or a configured restore_library when standby mode is not enabled"))); } /* diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index 8ef600ae72..c31d0f3bd2 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -46,6 +46,7 @@ #include "postmaster/bgwriter.h" #include "postmaster/interrupt.h" #include "replication/syncrep.h" +#include "restore/restore_module.h" #include "storage/bufmgr.h" #include "storage/condition_variable.h" #include "storage/fd.h" @@ -230,6 +231,25 @@ CheckpointerMain(char *startup_data, size_t startup_data_len) ALLOCSET_DEFAULT_SIZES); MemoryContextSwitchTo(checkpointer_context); + /* + * Load restore_library so that we can use its archive-cleanup callback, + * if one is defined. + * + * We also take this opportunity to set up the before_shmem_exit hook for + * the shutdown callback and to check that only one of restore_library, + * archive_cleanup_command is set. Note that we emit a WARNING upon + * detecting misconfigured parameters, and we clear the callbacks so that + * the archive-cleanup functionality is skipped. We judge this + * functionality to not be important enough to require blocking + * checkpoints or shutting down the server when the parameters are + * misconfigured. + */ + before_shmem_exit(call_restore_module_shutdown_cb, 0); + if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library", + archiveCleanupCommand, "archive_cleanup_command", + WARNING)) + LoadRestoreCallbacks(); + /* * If an exception is encountered, processing resumes here. * @@ -562,6 +582,10 @@ HandleCheckpointerInterrupts(void) if (ConfigReloadPending) { + bool archive_cleanup_settings_changed = false; + char *prevRestoreLibrary = pstrdup(restoreLibrary); + char *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand); + ConfigReloadPending = false; ProcessConfigFile(PGC_SIGHUP); @@ -577,6 +601,36 @@ HandleCheckpointerInterrupts(void) * because of SIGHUP. */ UpdateSharedMemoryConfig(); + + /* + * If the archive-cleanup settings have changed, call the currently + * loaded shutdown callback and clear all the restore callbacks. + * There's presently no good way to unload a library besides + * restarting the process, and there should be little harm in leaving + * it around, so we just leave it loaded. + */ + if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 || + strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0) + { + archive_cleanup_settings_changed = true; + call_restore_module_shutdown_cb(0, (Datum) 0); + RestoreCallbacks = NULL; + } + + /* + * As in CheckpointerMain(), we only emit a WARNING if we detect that + * both restore_library and archive_cleanup_command are set. We do + * this even if the archive-cleanup settings haven't changed to remind + * the user about the misconfiguration. + */ + if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library", + archiveCleanupCommand, "archive_cleanup_command", + WARNING) && + archive_cleanup_settings_changed) + LoadRestoreCallbacks(); + + pfree(prevRestoreLibrary); + pfree(prevArchiveCleanupCommand); } if (ShutdownRequestPending) { diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index cc665ce259..f8d3d6b3d2 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -26,6 +26,7 @@ #include "miscadmin.h" #include "postmaster/auxprocess.h" #include "postmaster/startup.h" +#include "restore/restore_module.h" #include "storage/ipc.h" #include "storage/pmsignal.h" #include "storage/procsignal.h" @@ -119,13 +120,17 @@ StartupProcShutdownHandler(SIGNAL_ARGS) * Re-read the config file. * * If one of the critical walreceiver options has changed, flag xlog.c - * to restart it. + * to restart it. Also, check for invalid combinations of the command/library + * parameters and reload the restore callbacks if necessary. */ static void StartupRereadConfig(void) { char *conninfo = pstrdup(PrimaryConnInfo); char *slotname = pstrdup(PrimarySlotName); + char *prevRestoreLibrary = pstrdup(restoreLibrary); + char *prevRestoreCommand = pstrdup(recoveryRestoreCommand); + char *prevRecoveryEndCommand = pstrdup(recoveryEndCommand); bool tempSlot = wal_receiver_create_temp_slot; bool conninfoChanged; bool slotnameChanged; @@ -147,6 +152,30 @@ StartupRereadConfig(void) if (conninfoChanged || slotnameChanged || tempSlotChanged) StartupRequestWalReceiverRestart(); + + /* + * If the restore settings have changed, call the currently loaded + * shutdown callback and load the new callbacks. There's presently no + * good way to unload a library besides restarting the process, and there + * should be little harm in leaving it around, so we just leave it loaded. + */ + (void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library", + recoveryRestoreCommand, "restore_command", + ERROR); + (void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library", + recoveryEndCommand, "recovery_end_command", + ERROR); + if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 || + strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 || + strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0) + { + call_restore_module_shutdown_cb(0, (Datum) 0); + LoadRestoreCallbacks(); + } + + pfree(prevRestoreLibrary); + pfree(prevRestoreCommand); + pfree(prevRecoveryEndCommand); } /* Handle various signals that might be sent to the startup process */ @@ -261,6 +290,19 @@ StartupProcessMain(char *startup_data, size_t startup_data_len) */ sigprocmask(SIG_SETMASK, &UnBlockSig, NULL); + /* + * Check for invalid combinations of the command/library parameters and + * load the callbacks. + */ + before_shmem_exit(call_restore_module_shutdown_cb, 0); + (void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library", + recoveryRestoreCommand, "restore_command", + ERROR); + (void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library", + recoveryEndCommand, "recovery_end_command", + ERROR); + LoadRestoreCallbacks(); + /* * Do what we came for. */ diff --git a/src/backend/restore/shell_restore.c b/src/backend/restore/shell_restore.c index 42291625c1..be25f04044 100644 --- a/src/backend/restore/shell_restore.c +++ b/src/backend/restore/shell_restore.c @@ -3,7 +3,8 @@ * shell_restore.c * * These restore functions use a user-specified shell command (e.g., the - * restore_command GUC). + * restore_command GUC). It is used as the default, but other modules may + * define their own restore logic. * * Copyright (c) 2024, PostgreSQL Global Development Group * @@ -22,25 +23,76 @@ #include "common/archive.h" #include "common/percentrepl.h" #include "postmaster/startup.h" +#include "restore/restore_module.h" #include "restore/shell_restore.h" #include "storage/ipc.h" #include "utils/wait_event.h" +static void shell_restore_startup(RestoreModuleState *state); +static bool shell_restore_file_configured(RestoreModuleState *state); static bool shell_restore_file(const char *file, const char *path, const char *lastRestartPointFileName); +static bool shell_restore_wal_segment(RestoreModuleState *state, + const char *file, const char *path, + const char *lastRestartPointFileName); +static bool shell_restore_timeline_history(RestoreModuleState *state, + const char *file, const char *path); +static bool shell_archive_cleanup_configured(RestoreModuleState *state); +static void shell_archive_cleanup(RestoreModuleState *state, + const char *lastRestartPointFileName); +static bool shell_recovery_end_configured(RestoreModuleState *state); +static void shell_recovery_end(RestoreModuleState *state, + const char *lastRestartPointFileName); static void ExecuteRecoveryCommand(const char *command, const char *commandName, bool failOnSignal, uint32 wait_event_info, const char *lastRestartPointFileName); +static const RestoreModuleCallbacks shell_restore_callbacks = { + .startup_cb = shell_restore_startup, + .restore_wal_segment_configured_cb = shell_restore_file_configured, + .restore_wal_segment_cb = shell_restore_wal_segment, + .restore_timeline_history_configured_cb = shell_restore_file_configured, + .restore_timeline_history_cb = shell_restore_timeline_history, + .timeline_history_exists_configured_cb = shell_restore_file_configured, + .timeline_history_exists_cb = shell_restore_timeline_history, + .archive_cleanup_configured_cb = shell_archive_cleanup_configured, + .archive_cleanup_cb = shell_archive_cleanup, + .recovery_end_configured_cb = shell_recovery_end_configured, + .recovery_end_cb = shell_recovery_end, + .shutdown_cb = NULL +}; + +/* + * shell_restore_init + * + * Returns the callbacks for restoring via shell. + */ +const RestoreModuleCallbacks * +shell_restore_init(void) +{ + return &shell_restore_callbacks; +} + +/* + * Indicates that our timeline_history_exists_cb only attempts to retrieve the + * file, so the caller should verify the file exists afterwards. + */ +static void +shell_restore_startup(RestoreModuleState *state) +{ + state->timeline_history_exists_cb_copies = true; +} + /* * Attempt to execute a shell-based restore command to retrieve a WAL segment. * * Returns true if the command has succeeded, false otherwise. */ -bool -shell_restore_wal_segment(const char *file, const char *path, +static bool +shell_restore_wal_segment(RestoreModuleState *state, + const char *file, const char *path, const char *lastRestartPointFileName) { return shell_restore_file(file, path, lastRestartPointFileName); @@ -52,8 +104,9 @@ shell_restore_wal_segment(const char *file, const char *path, * * Returns true if the command has succeeded, false otherwise. */ -bool -shell_restore_timeline_history(const char *file, const char *path) +static bool +shell_restore_timeline_history(RestoreModuleState *state, + const char *file, const char *path) { char lastRestartPointFname[MAXPGPATH]; @@ -64,8 +117,8 @@ shell_restore_timeline_history(const char *file, const char *path) /* * Check whether restore_command is supplied. */ -bool -shell_restore_file_configured(void) +static bool +shell_restore_file_configured(RestoreModuleState *state) { return recoveryRestoreCommand[0] != '\0'; } @@ -152,8 +205,8 @@ shell_restore_file(const char *file, const char *path, /* * Check whether archive_cleanup_command is supplied. */ -bool -shell_archive_cleanup_configured(void) +static bool +shell_archive_cleanup_configured(RestoreModuleState *state) { return archiveCleanupCommand[0] != '\0'; } @@ -161,8 +214,9 @@ shell_archive_cleanup_configured(void) /* * Attempt to execute a shell-based archive cleanup command. */ -void -shell_archive_cleanup(const char *lastRestartPointFileName) +static void +shell_archive_cleanup(RestoreModuleState *state, + const char *lastRestartPointFileName) { ExecuteRecoveryCommand(archiveCleanupCommand, "archive_cleanup_command", false, WAIT_EVENT_ARCHIVE_CLEANUP_COMMAND, @@ -172,8 +226,8 @@ shell_archive_cleanup(const char *lastRestartPointFileName) /* * Check whether recovery_end_command is supplied. */ -bool -shell_recovery_end_configured(void) +static bool +shell_recovery_end_configured(RestoreModuleState *state) { return recoveryEndCommand[0] != '\0'; } @@ -181,8 +235,9 @@ shell_recovery_end_configured(void) /* * Attempt to execute a shell-based end-of-recovery command. */ -void -shell_recovery_end(const char *lastRestartPointFileName) +static void +shell_recovery_end(RestoreModuleState *state, + const char *lastRestartPointFileName) { ExecuteRecoveryCommand(recoveryEndCommand, "recovery_end_command", true, WAIT_EVENT_RECOVERY_END_COMMAND, diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index ea2b0577bc..89d0756fa2 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -70,6 +70,7 @@ #include "replication/slot.h" #include "replication/slotsync.h" #include "replication/syncrep.h" +#include "restore/restore_module.h" #include "storage/bufmgr.h" #include "storage/large_object.h" #include "storage/pg_shmem.h" @@ -3967,6 +3968,16 @@ struct config_string ConfigureNamesString[] = NULL, NULL, NULL }, + { + {"restore_library", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY, + gettext_noop("Sets the library that will be called for restore actions."), + NULL + }, + &restoreLibrary, + "", + NULL, NULL, NULL + }, + { {"archive_cleanup_command", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY, gettext_noop("Sets the shell command that will be executed at every restart point."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 83d5df8e46..a9a2475c4b 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -285,6 +285,7 @@ # placeholders: %p = path of file to restore # %f = file name only # e.g. 'cp /mnt/server/archivedir/%f %p' +#restore_library = '' # library to use for restore actions #archive_cleanup_command = '' # command to execute at every restartpoint #recovery_end_command = '' # command to execute at completion of recovery diff --git a/src/include/restore/restore_module.h b/src/include/restore/restore_module.h new file mode 100644 index 0000000000..cc148e855a --- /dev/null +++ b/src/include/restore/restore_module.h @@ -0,0 +1,143 @@ +/*------------------------------------------------------------------------- + * + * restore_module.h + * Exports for restore modules. + * + * Copyright (c) 2024, PostgreSQL Global Development Group + * + * src/include/restore/restore_module.h + * + *------------------------------------------------------------------------- + */ +#ifndef _RESTORE_MODULE_H +#define _RESTORE_MODULE_H + +/* + * The value of the restore_library GUC. + */ +extern PGDLLIMPORT char *restoreLibrary; + +typedef struct RestoreModuleState +{ + /* + * Private data pointer for use by a restore module. This can be used to + * store state for the module that will be passed to each of its + * callbacks. + */ + void *private_data; + + /* + * Indicates whether the callback that checks for timeline existence + * merely copies the file. If set to true, the server will verify that + * the timeline history file exists after the callback returns. + */ + bool timeline_history_exists_cb_copies; +} RestoreModuleState; + +/* + * Restore module callbacks + * + * These callback functions should be defined by restore libraries and returned + * via _PG_restore_module_init(). For more information about the purpose of + * each callback, refer to the restore modules documentation. + */ +typedef void (*RestoreStartupCB) (RestoreModuleState *state); +typedef bool (*RestoreWalSegmentConfiguredCB) (RestoreModuleState *state); +typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state, + const char *file, const char *path, + const char *lastRestartPointFileName); +typedef bool (*RestoreTimelineHistoryConfiguredCB) (RestoreModuleState *state); +typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state, + const char *file, const char *path); +typedef bool (*TimelineHistoryExistsConfiguredCB) (RestoreModuleState *state); +typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state, + const char *file, const char *path); +typedef bool (*ArchiveCleanupConfiguredCB) (RestoreModuleState *state); +typedef void (*ArchiveCleanupCB) (RestoreModuleState *state, + const char *lastRestartPointFileName); +typedef bool (*RecoveryEndConfiguredCB) (RestoreModuleState *state); +typedef void (*RecoveryEndCB) (RestoreModuleState *state, + const char *lastRestartPointFileName); +typedef void (*RestoreShutdownCB) (RestoreModuleState *state); + +typedef struct RestoreModuleCallbacks +{ + RestoreStartupCB startup_cb; + RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb; + RestoreWalSegmentCB restore_wal_segment_cb; + RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb; + RestoreTimelineHistoryCB restore_timeline_history_cb; + TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb; + TimelineHistoryExistsCB timeline_history_exists_cb; + ArchiveCleanupConfiguredCB archive_cleanup_configured_cb; + ArchiveCleanupCB archive_cleanup_cb; + RecoveryEndConfiguredCB recovery_end_configured_cb; + RecoveryEndCB recovery_end_cb; + RestoreShutdownCB shutdown_cb; +} RestoreModuleCallbacks; + +/* + * Type of the shared library symbol _PG_restore_module_init that is looked up + * when loading a restore library. + */ +typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void); + +extern PGDLLEXPORT const RestoreModuleCallbacks *_PG_restore_module_init(void); + +extern void LoadRestoreCallbacks(void); +extern void call_restore_module_shutdown_cb(int code, Datum arg); + +extern const RestoreModuleCallbacks *RestoreCallbacks; +extern RestoreModuleState *restore_module_state; + +/* + * The following *_configured() functions are convenient wrappers around the + * *_configured_cb() callback functions with additional defensive NULL checks. + */ + +static inline bool +restore_wal_segment_configured(void) +{ + return RestoreCallbacks != NULL && + RestoreCallbacks->restore_wal_segment_configured_cb != NULL && + RestoreCallbacks->restore_wal_segment_cb != NULL && + RestoreCallbacks->restore_wal_segment_configured_cb(restore_module_state); +} + +static inline bool +restore_timeline_history_configured(void) +{ + return RestoreCallbacks != NULL && + RestoreCallbacks->restore_timeline_history_configured_cb != NULL && + RestoreCallbacks->restore_timeline_history_cb != NULL && + RestoreCallbacks->restore_timeline_history_configured_cb(restore_module_state); +} + +static inline bool +timeline_history_exists_configured(void) +{ + return RestoreCallbacks != NULL && + RestoreCallbacks->timeline_history_exists_configured_cb != NULL && + RestoreCallbacks->timeline_history_exists_cb != NULL && + RestoreCallbacks->timeline_history_exists_configured_cb(restore_module_state); +} + +static inline bool +archive_cleanup_configured(void) +{ + return RestoreCallbacks != NULL && + RestoreCallbacks->archive_cleanup_configured_cb != NULL && + RestoreCallbacks->archive_cleanup_cb != NULL && + RestoreCallbacks->archive_cleanup_configured_cb(restore_module_state); +} + +static inline bool +recovery_end_configured(void) +{ + return RestoreCallbacks != NULL && + RestoreCallbacks->recovery_end_configured_cb != NULL && + RestoreCallbacks->recovery_end_cb != NULL && + RestoreCallbacks->recovery_end_configured_cb(restore_module_state); +} + +#endif /* _RESTORE_MODULE_H */ diff --git a/src/include/restore/shell_restore.h b/src/include/restore/shell_restore.h index 28b5b7bc92..6352623866 100644 --- a/src/include/restore/shell_restore.h +++ b/src/include/restore/shell_restore.h @@ -12,15 +12,13 @@ #ifndef _SHELL_RESTORE_H #define _SHELL_RESTORE_H -extern bool shell_restore_file_configured(void); -extern bool shell_restore_wal_segment(const char *file, const char *path, - const char *lastRestartPointFileName); -extern bool shell_restore_timeline_history(const char *file, const char *path); +#include "restore/restore_module.h" -extern bool shell_archive_cleanup_configured(void); -extern void shell_archive_cleanup(const char *lastRestartPointFileName); - -extern bool shell_recovery_end_configured(void); -extern void shell_recovery_end(const char *lastRestartPointFileName); +/* + * Since the logic for restoring via shell commands is in the core server and + * does not need to be loaded via a shared library, it has a special + * initialization function. + */ +extern const RestoreModuleCallbacks *shell_restore_init(void); #endif /* _SHELL_RESTORE_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 886a49de6f..6eae455775 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2450,6 +2450,8 @@ ResourceReleaseCallback ResourceReleaseCallbackItem ResourceReleasePhase ResourceReleasePriority +RestoreModuleCallbacks +RestoreModuleState RestoreOptions RestorePass RestrictInfo -- 2.25.1