rebased
--
Nathan Bossart
Amazon Web Services: https://aws.amazon.com
>From 6cee7d220b886e9be309929da5274c5770e59845 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <[email protected]>
Date: Wed, 15 Feb 2023 14:28:53 -0800
Subject: [PATCH v17 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 67693b0580..6ae8bb53c4 100644
--- a/src/backend/postmaster/pgarch.c
+++ b/src/backend/postmaster/pgarch.c
@@ -824,11 +824,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 8f65ef3d89..9d21c16169 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -2640,6 +2640,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 471d53da8f..6543e61463 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -375,6 +375,9 @@ extern int NewGUCNestLevel(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 9f5c2602baa8e29058bc976c01516d04e3c1f901 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <[email protected]>
Date: Wed, 15 Feb 2023 10:36:00 -0800
Subject: [PATCH v17 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 7d2ea7d54a..cbeb8ac93c 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 478377c4a2..957b406272 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -85,6 +85,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"
@@ -703,6 +704,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);
@@ -5081,12 +5083,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.
@@ -7564,12 +7572,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;
}
@@ -9199,6 +9213,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 1292b11f2e..d2d4a38a0c 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/startup.h"
#include "postmaster/pgarch.h"
#include "replication/walsender.h"
+#include "restore/shell_restore.h"
#include "storage/fd.h"
#include "storage/ipc.h"
#include "storage/lwlock.h"
@@ -54,12 +54,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;
@@ -73,8 +72,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
@@ -150,39 +162,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
@@ -238,37 +244,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:
/*
@@ -282,74 +257,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 1b48d7171a..be5edf4beb 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -4157,7 +4157,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 8767aaba67..719c14bc1a 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 d53c37d062..eaa7e0451d 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -197,8 +197,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
@@ -222,6 +221,16 @@ HandleStartupProcInterrupts(void)
ProcessLogMemoryContextInterrupt();
}
+/*
+ * If there is a pending shutdown request, exit.
+ */
+void
+HandleStartupProcShutdownRequests(void)
+{
+ if (shutdown_requested)
+ proc_exit(1);
+}
+
/* --------------------------------
* signal handler routines
@@ -297,8 +306,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 11dd6f0d77..528c5a804d 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 cf7a43e38c..d2662fa58a 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(void) 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 5fd46b7bd1..2f33dff7e5 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -135,6 +135,7 @@ ArchiveOpts
ArchiveShutdownCB
ArchiveStartupCB
ArchiveStreamState
+ArchiveType
ArchiverOutput
ArchiverStage
ArrayAnalyzeExtraData
--
2.25.1
>From b975836929b477ae6bdf8d4673602538869c9f96 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <[email protected]>
Date: Wed, 15 Feb 2023 14:45:17 -0800
Subject: [PATCH v17 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 bb4926b887..89bc9c689a 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 2c107199d3..23522ccaf3 100644
--- a/doc/src/sgml/postgres.sgml
+++ b/doc/src/sgml/postgres.sgml
@@ -228,7 +228,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 d16feb7b409162600c931c52db4ccfacd826cdea Mon Sep 17 00:00:00 2001
From: Nathan Bossart <[email protected]>
Date: Wed, 15 Feb 2023 14:48:36 -0800
Subject: [PATCH v17 4/5] restructure archive modules docs in preparation for
restore modules
---
doc/src/sgml/archive-and-restore-modules.sgml | 206 ++++++++++--------
1 file changed, 110 insertions(+), 96 deletions(-)
diff --git a/doc/src/sgml/archive-and-restore-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml
index 7064307d9e..7ed50a3b52 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,91 +72,98 @@ 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>
- </sect2>
+ In the latter case, the server will periodically call this function, and
+ archiving will proceed only when it returns <literal>true</literal>.
+ </para>
+ </sect3>
- <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.
+ <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, 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>
- </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.
+ 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, 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>
+ </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 df6bfcfcf0df3bd3b26ab216a6f25072d6aab0ab Mon Sep 17 00:00:00 2001
From: Nathan Bossart <[email protected]>
Date: Wed, 15 Feb 2023 22:06:04 -0800
Subject: [PATCH v17 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 804567e919..00d7f1470e 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"
@@ -54,6 +60,16 @@ static void basic_archive_file_internal(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 void basic_archive_shutdown(ArchiveModuleState *state);
+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 = basic_archive_startup,
@@ -62,6 +78,21 @@ static const ArchiveModuleCallbacks basic_archive_callbacks = {
.shutdown_cb = basic_archive_shutdown
};
+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
*
@@ -71,7 +102,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,
"",
@@ -93,6 +124,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;
+}
+
/*
* basic_archive_startup
*
@@ -426,3 +468,112 @@ basic_archive_shutdown(ArchiveModuleState *state)
pfree(data);
state->private_data = NULL;
}
+
+/*
+ * 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 7ed50a3b52..bf2fdb313a 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">
@@ -166,4 +168,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 b3468eea3c..e1643f92e0 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1261,9 +1261,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.
@@ -1282,14 +1299,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>
@@ -1313,7 +1336,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 61038472c5..290903c6f5 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3767,7 +3767,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
@@ -3795,7 +3796,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,
@@ -3830,7 +3832,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>
@@ -3875,7 +3912,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>
@@ -3904,7 +3944,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 9dd52ff275..96817fdc78 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 957b406272..a6c59307b0 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -85,7 +85,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"
@@ -5082,18 +5082,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);
}
/*
@@ -7571,18 +7572,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 d2d4a38a0c..4b321878e9 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -23,15 +23,22 @@
#include "access/xlog_internal.h"
#include "access/xlogarchive.h"
#include "common/archive.h"
+#include "fmgr.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "postmaster/startup.h"
#include "postmaster/pgarch.h"
#include "replication/walsender.h"
+#include "restore/restore_module.h"
#include "restore/shell_restore.h"
#include "storage/fd.h"
#include "storage/ipc.h"
#include "storage/lwlock.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.
@@ -75,15 +82,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;
}
@@ -175,14 +182,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;
}
@@ -190,6 +200,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.
@@ -631,3 +651,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 be5edf4beb..d2f5e39d09 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -50,6 +50,7 @@
#include "postmaster/startup.h"
#include "replication/slot.h"
#include "replication/walreceiver.h"
+#include "restore/restore_module.h"
#include "storage/fd.h"
#include "storage/ipc.h"
#include "storage/latch.h"
@@ -1088,18 +1089,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 5e949fc885..557fc496fe 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -45,6 +45,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"
@@ -224,6 +225,25 @@ CheckpointerMain(void)
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.
*
@@ -561,6 +581,10 @@ HandleCheckpointerInterrupts(void)
if (ConfigReloadPending)
{
+ bool archive_cleanup_settings_changed = false;
+ char *prevRestoreLibrary = pstrdup(restoreLibrary);
+ char *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
ConfigReloadPending = false;
ProcessConfigFile(PGC_SIGHUP);
@@ -576,6 +600,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 eaa7e0451d..de18a6db1a 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -28,6 +28,7 @@
#include "miscadmin.h"
#include "pgstat.h"
#include "postmaster/startup.h"
+#include "restore/restore_module.h"
#include "storage/ipc.h"
#include "storage/latch.h"
#include "storage/pmsignal.h"
@@ -147,13 +148,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;
@@ -175,6 +180,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 */
@@ -284,6 +313,19 @@ StartupProcessMain(void)
*/
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 e53ebc6dc2..7b1ea733ee 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -68,6 +68,7 @@
#include "replication/logicallauncher.h"
#include "replication/slot.h"
#include "replication/syncrep.h"
+#include "restore/restore_module.h"
#include "storage/bufmgr.h"
#include "storage/large_object.h"
#include "storage/pg_shmem.h"
@@ -3884,6 +3885,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 835b0e9ba8..99de17a08e 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -275,6 +275,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 2f33dff7e5..4a756c92d8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2378,6 +2378,8 @@ ResourceOwnerDesc
ResourceReleaseCallback
ResourceReleaseCallbackItem
ResourceReleasePhase
+RestoreModuleCallbacks
+RestoreModuleState
RestoreOptions
RestorePass
RestrictInfo
--
2.25.1