Here is a new revision of the restore modules patch set.  In this patch
set, the interface looks similar to the recent archive modules redesign,
and there are separate callbacks for retrieving different types of files.
I've attempted to address all the feedback I've received, but there was a
lot scattered across different threads, so it's possible I've missed
something.  Note that 0001 is the stopgap fix for restore_command that's
being tracked elsewhere [0].  I was careful to avoid repeating the recent
mistake with the SIGTERM handling.

This patch set is still a little rough around the edges, but I wanted to
post it in case folks had general thoughts about the structure, interface,
etc.  This implementation restores files synchronously one-by-one just like
archive modules, but in the future, I would like to add
asynchronous/parallel/batching support.  My intent is for this work to move
us closer to that.

[0] https://postgr.es/m/20230214174755.GA1348509%40nathanxps13

-- 
Nathan Bossart
Amazon Web Services: https://aws.amazon.com
>From 64651ae3c56160fea31d15a78f07c8c12950ec99 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathandboss...@gmail.com>
Date: Tue, 14 Feb 2023 09:44:53 -0800
Subject: [PATCH v12 1/6] stopgap fix for restore_command

---
 src/backend/access/transam/xlogarchive.c | 15 +++++++++++----
 src/backend/postmaster/startup.c         | 20 +++++++++++++++++++-
 src/backend/storage/ipc/ipc.c            |  3 +++
 src/backend/storage/lmgr/proc.c          |  2 ++
 4 files changed, 35 insertions(+), 5 deletions(-)

diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index fcc87ff44f..41684418b6 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -159,20 +159,27 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 			(errmsg_internal("executing restore command \"%s\"",
 							 xlogRestoreCmd)));
 
+	fflush(NULL);
+	pgstat_report_wait_start(WAIT_EVENT_RESTORE_COMMAND);
+
 	/*
-	 * Check signals before restore command and reset afterwards.
+	 * 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
 	 */
-	fflush(NULL);
-	pgstat_report_wait_start(WAIT_EVENT_RESTORE_COMMAND);
 	rc = system(xlogRestoreCmd);
-	pgstat_report_wait_end();
 
 	PostRestoreCommand();
+
+	pgstat_report_wait_end();
 	pfree(xlogRestoreCmd);
 
 	if (rc == 0)
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index efc2580536..de2b56c2fa 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -19,6 +19,8 @@
  */
 #include "postgres.h"
 
+#include <unistd.h>
+
 #include "access/xlog.h"
 #include "access/xlogrecovery.h"
 #include "access/xlogutils.h"
@@ -121,7 +123,23 @@ StartupProcShutdownHandler(SIGNAL_ARGS)
 	int			save_errno = errno;
 
 	if (in_restore_command)
-		proc_exit(1);
+	{
+		/*
+		 * If we are in a child process (e.g., forked by system() in
+		 * RestoreArchivedFile()), we don't want to call any exit callbacks.
+		 * The parent will take care of that.
+		 */
+		if (MyProcPid == (int) getpid())
+			proc_exit(1);
+		else
+		{
+			const char	msg[] = "StartupProcShutdownHandler() called in child process";
+			int			rc pg_attribute_unused();
+
+			rc = write(STDERR_FILENO, msg, sizeof(msg));
+			_exit(1);
+		}
+	}
 	else
 		shutdown_requested = true;
 	WakeupRecovery();
diff --git a/src/backend/storage/ipc/ipc.c b/src/backend/storage/ipc/ipc.c
index 1904d21795..6796cabc3e 100644
--- a/src/backend/storage/ipc/ipc.c
+++ b/src/backend/storage/ipc/ipc.c
@@ -103,6 +103,9 @@ static int	on_proc_exit_index,
 void
 proc_exit(int code)
 {
+	/* proc_exit() is not safe in forked processes from system(), etc. */
+	Assert(MyProcPid == getpid());
+
 	/* Clean up everything that must be cleaned up */
 	proc_exit_prepare(code);
 
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 22b4278610..ae845e8249 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -805,6 +805,7 @@ ProcKill(int code, Datum arg)
 	dlist_head *procgloballist;
 
 	Assert(MyProc != NULL);
+	Assert(MyProcPid == getpid());  /* not safe if forked by system(), etc. */
 
 	/* Make sure we're out of the sync rep lists */
 	SyncRepCleanupAtProcExit();
@@ -925,6 +926,7 @@ AuxiliaryProcKill(int code, Datum arg)
 	PGPROC	   *proc;
 
 	Assert(proctype >= 0 && proctype < NUM_AUXILIARY_PROCS);
+	Assert(MyProcPid == getpid());	/* not safe if forked by system(), etc. */
 
 	auxproc = &AuxiliaryProcs[proctype];
 
-- 
2.25.1

>From 2ef84ed51ba934c7979e2a741981e5200ca6f093 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathandboss...@gmail.com>
Date: Wed, 15 Feb 2023 14:28:53 -0800
Subject: [PATCH v12 2/6] 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 46af349564..d94730c50c 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 51e07d5582..e780438948 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -2610,6 +2610,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 ba89d013e6..7346a3f1ea 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -372,6 +372,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 e5b48074800b60cb567e17c7cd14979ec6512de7 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathandboss...@gmail.com>
Date: Wed, 15 Feb 2023 10:36:00 -0800
Subject: [PATCH v12 3/6] 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       | 246 ++++++++++++++++++++++
 src/include/access/xlogarchive.h          |   9 +-
 src/include/postmaster/startup.h          |   1 +
 src/include/restore/shell_restore.h       |  26 +++
 src/tools/pgindent/typedefs.list          |   1 +
 14 files changed, 406 insertions(+), 151 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 e4bf0fe9c0..23812e9a6a 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -20,7 +20,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 94e152694e..b3e1c18c65 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 f9f0f6db8d..c4268af688 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -84,6 +84,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"
@@ -692,6 +693,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);
@@ -4887,12 +4889,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.
@@ -7307,12 +7315,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;
 }
@@ -8889,6 +8903,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 41684418b6..4b45ea8753 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, 0L, 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 dbe9394762..f0e1007f92 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -4106,7 +4106,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 4fdd209b82..d5d3ae7822 100644
--- a/src/backend/meson.build
+++ b/src/backend/meson.build
@@ -26,6 +26,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 de2b56c2fa..4648299ced 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -201,8 +201,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
@@ -226,6 +225,16 @@ HandleStartupProcInterrupts(void)
 		ProcessLogMemoryContextInterrupt();
 }
 
+/*
+ * If there is a pending shutdown request, exit.
+ */
+void
+HandleStartupProcShutdownRequests(void)
+{
+	if (shutdown_requested)
+		proc_exit(1);
+}
+
 
 /* --------------------------------
  *		signal handler routines
@@ -301,8 +310,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..2b27cfced0
--- /dev/null
+++ b/src/backend/restore/meson.build
@@ -0,0 +1,5 @@
+# Copyright (c) 2023, 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..fcd5475c88
--- /dev/null
+++ b/src/backend/restore/shell_restore.c
@@ -0,0 +1,246 @@
+/*-------------------------------------------------------------------------
+ *
+ * shell_restore.c
+ *
+ * These restore functions use a user-specified shell command (e.g., the
+ * restore_command GUC).
+ *
+ * Copyright (c) 2023, 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);
+
+	/*
+	 * Check signals before restore command and reset afterwards.
+	 * 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/access/xlogarchive.h b/src/include/access/xlogarchive.h
index 31ff206034..b9bd0a93cd 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/postmaster/startup.h b/src/include/postmaster/startup.h
index 6a2e4c4526..09d3f89c69 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..570588e493
--- /dev/null
+++ b/src/include/restore/shell_restore.h
@@ -0,0 +1,26 @@
+/*-------------------------------------------------------------------------
+ *
+ * shell_restore.h
+ *		Exports for restoring via shell.
+ *
+ * Copyright (c) 2023, 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 22ea42c16b..931c9acf45 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -132,6 +132,7 @@ ArchiveModuleState
 ArchiveOpts
 ArchiveShutdownCB
 ArchiveStreamState
+ArchiveType
 ArchiverOutput
 ArchiverStage
 ArrayAnalyzeExtraData
-- 
2.25.1

>From b9b14e1394e738f2a09bb8532d6deb3bc7578df2 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathandboss...@gmail.com>
Date: Wed, 15 Feb 2023 14:45:17 -0800
Subject: [PATCH v12 4/6] 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 0d6be9a2fa..66a4de4762 100644
--- a/doc/src/sgml/filelist.sgml
+++ b/doc/src/sgml/filelist.sgml
@@ -100,7 +100,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 2e271862fc..f1ff6bb451 100644
--- a/doc/src/sgml/postgres.sgml
+++ b/doc/src/sgml/postgres.sgml
@@ -233,7 +233,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 2a966fa63f61e50c878ce198bf1fb59bd5878f1a Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathandboss...@gmail.com>
Date: Wed, 15 Feb 2023 14:48:36 -0800
Subject: [PATCH v12 5/6] restructure archive modules docs in preparation for
 restore modules

---
 doc/src/sgml/archive-and-restore-modules.sgml | 207 ++++++++++--------
 1 file changed, 111 insertions(+), 96 deletions(-)

diff --git a/doc/src/sgml/archive-and-restore-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml
index 7cf44e82e2..f30ee591e7 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,53 @@
   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 how 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 how 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 +73,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 a 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 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 a 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 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 20fd226d8842528dba5169a908a8ee163f4a7816 Mon Sep 17 00:00:00 2001
From: Nathan Bossart <nathandboss...@gmail.com>
Date: Wed, 15 Feb 2023 22:06:04 -0800
Subject: [PATCH v12 6/6] introduce restore_library

---
 contrib/basic_archive/Makefile                |   1 +
 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             |  28 +-
 src/backend/access/transam/xlogarchive.c      |  96 ++++-
 src/backend/access/transam/xlogrecovery.c     |  14 +-
 src/backend/postmaster/checkpointer.c         |  53 +++
 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          | 142 +++++++
 src/include/restore/shell_restore.h           |  16 +-
 19 files changed, 1103 insertions(+), 88 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 55d299d650..d12d45e0d2 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -5,6 +5,7 @@ PGFILEDESC = "basic_archive - basic archive module"
 
 REGRESS = basic_archive
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.conf
+TAP_TESTS = 1
 # Disabled because these tests require "shared_preload_libraries=basic_archive",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index cd852888ce..b04d83da4c 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-2023, 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 bc1380e6f6..4e9f5002de 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -31,4 +31,9 @@ tests += {
     # which typical runningcheck 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..85b14c5113
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,44 @@
+
+# Copyright (c) 2023, 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 was replayed
+my $result = $restore->safe_psql("postgres", "SELECT count(*) 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 f30ee591e7..56373a211b 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">
@@ -167,4 +169,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 be05a33205..5555f23a85 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1180,9 +1180,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.
@@ -1201,14 +1218,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>
@@ -1232,7 +1255,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 ecd9aa73ef..8fec106a24 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3807,7 +3807,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
@@ -3835,7 +3836,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,
@@ -3870,7 +3872,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>
@@ -3915,7 +3952,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>
@@ -3944,7 +3984,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 f180607528..957d427616 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 c4268af688..b6fa39c7cf 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -84,7 +84,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"
@@ -4888,18 +4888,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 log
-	 * shipping replication.  All files earlier than this point can be deleted
-	 * from the archive, though there is no requirement to do so.
+	 * 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);
 	}
 
 	/*
@@ -7314,18 +7315,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 log
-	 * shipping replication.  All files earlier than this point can be deleted
-	 * from the archive, though there is no requirement to do so.
+	 * 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 4b45ea8753..50bcda1120 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 f0e1007f92..c6645c2413 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"
@@ -1078,18 +1079,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 aaad5c5228..73ff592f9f 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"
@@ -233,6 +234,24 @@ 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.
 	 *
@@ -548,6 +567,10 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		bool		archive_cleanup_settings_changed = false;
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -563,6 +586,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 4648299ced..91229d96be 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -29,6 +29,7 @@
 #include "pgstat.h"
 #include "postmaster/interrupt.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
 #include "storage/pmsignal.h"
@@ -151,13 +152,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;
@@ -179,6 +184,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 */
@@ -288,6 +317,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 fcd5475c88..b67cd4ac48 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) 2023, 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';
 }
@@ -153,8 +206,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';
 }
@@ -162,8 +215,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,
@@ -173,8 +227,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';
 }
@@ -182,8 +236,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 1c0583fe26..a42819399b 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -63,6 +63,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"
@@ -3788,6 +3789,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 d06074b86f..53757fea5f 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -270,6 +270,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..c0aef33ba5
--- /dev/null
+++ b/src/include/restore/restore_module.h
@@ -0,0 +1,142 @@
+/*-------------------------------------------------------------------------
+ *
+ * restore_module.h
+ *		Exports for restore modules.
+ *
+ * Copyright (c) 2023, 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 570588e493..1eeae51d81 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 */
-- 
2.25.1

Reply via email to