public inbox for [email protected]  
help / color / mirror / Atom feed
[PATCH v9 1/1] Allow recovery via loadable modules.
17+ messages / 3 participants
[nested] [flat]

* [PATCH v6 3/3] Allow recovery via loadable modules.
@ 2022-12-10 03:40 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 17+ messages in thread

From: Nathan Bossart @ 2022-12-10 03:40 UTC (permalink / raw)

This adds the restore_library parameter to allow archive recovery
via a loadable module, rather than running shell commands.
---
 contrib/basic_archive/Makefile                |   4 +-
 contrib/basic_archive/basic_archive.c         |  67 ++++++-
 contrib/basic_archive/meson.build             |   7 +-
 contrib/basic_archive/t/001_restore.pl        |  44 +++++
 doc/src/sgml/archive-modules.sgml             | 168 ++++++++++++++++--
 doc/src/sgml/backup.sgml                      |  43 ++++-
 doc/src/sgml/basic-archive.sgml               |  33 ++--
 doc/src/sgml/config.sgml                      |  54 +++++-
 doc/src/sgml/high-availability.sgml           |  23 ++-
 src/backend/access/transam/shell_restore.c    |  21 ++-
 src/backend/access/transam/xlog.c             |  13 +-
 src/backend/access/transam/xlogarchive.c      |  70 +++++++-
 src/backend/access/transam/xlogrecovery.c     |  26 ++-
 src/backend/postmaster/checkpointer.c         |  26 +++
 src/backend/postmaster/pgarch.c               |   7 +-
 src/backend/postmaster/startup.c              |  23 ++-
 src/backend/utils/misc/guc.c                  |  14 ++
 src/backend/utils/misc/guc_tables.c           |  10 ++
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/access/xlog_internal.h            |   1 +
 src/include/access/xlogarchive.h              |  44 ++++-
 src/include/access/xlogrecovery.h             |   1 +
 src/include/utils/guc.h                       |   2 +
 23 files changed, 618 insertions(+), 84 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 55d299d650..487dc563f3 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -1,7 +1,7 @@
 # contrib/basic_archive/Makefile
 
 MODULES = basic_archive
-PGFILEDESC = "basic_archive - basic archive module"
+PGFILEDESC = "basic_archive - basic archive and recovery module"
 
 REGRESS = basic_archive
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.conf
@@ -9,6 +9,8 @@ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.c
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
 
+TAP_TESTS = 1
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index 28cbb6cce0..8c333c8f99 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
@@ -30,6 +35,7 @@
 #include <sys/time.h>
 #include <unistd.h>
 
+#include "access/xlogarchive.h"
 #include "common/int.h"
 #include "miscadmin.h"
 #include "postmaster/pgarch.h"
@@ -48,6 +54,8 @@ static bool basic_archive_file(const char *file, const char *path);
 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 bool basic_restore_file(const char *file, const char *path,
+							   const char *lastRestartPointFileName);
 
 /*
  * _PG_init
@@ -87,6 +95,19 @@ _PG_archive_module_init(ArchiveModuleCallbacks *cb)
 	cb->archive_file_cb = basic_archive_file;
 }
 
+/*
+ * _PG_recovery_module_init
+ *
+ * Returns the module's restore callback.
+ */
+void
+_PG_recovery_module_init(RecoveryModuleCallbacks *cb)
+{
+	AssertVariableIsOfType(&_PG_recovery_module_init, RecoveryModuleInit);
+
+	cb->restore_cb = basic_restore_file;
+}
+
 /*
  * check_archive_directory
  *
@@ -99,8 +120,8 @@ check_archive_directory(char **newval, void **extra, GucSource source)
 
 	/*
 	 * The default value is an empty string, so we have to accept that value.
-	 * Our check_configured callback also checks for this and prevents
-	 * archiving from proceeding if it is still empty.
+	 * Our check_configured and restore callbacks also check for this and
+	 * prevent archiving or recovery from proceeding if it is still empty.
 	 */
 	if (*newval == NULL || *newval[0] == '\0')
 		return true;
@@ -368,3 +389,45 @@ compare_files(const char *file1, const char *file2)
 
 	return ret;
 }
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file from the WAL archives.
+ */
+static bool
+basic_restore_file(const char *file, const char *path,
+				   const char *lastRestartPointFileName)
+{
+	char		source[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive", file)));
+
+	if (archive_directory == NULL || archive_directory[0] == '\0')
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"basic_archive.archive_directory\" is not set")));
+
+	/*
+	 * Check whether the file exists.  If not, we return false to indicate that
+	 * there are no more files to restore.
+	 */
+	snprintf(source, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(source, &st) != 0)
+	{
+		int		elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m", source)));
+		return false;
+	}
+
+	copy_file(source, unconstify(char *, path));
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive", file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index bc1380e6f6..af4580dea9 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -7,7 +7,7 @@ basic_archive_sources = files(
 if host_system == 'windows'
   basic_archive_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
     '--NAME', 'basic_archive',
-    '--FILEDESC', 'basic_archive - basic archive module',])
+    '--FILEDESC', 'basic_archive - basic archive and recovery module',])
 endif
 
 basic_archive = shared_module('basic_archive',
@@ -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..ec8767d740
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,44 @@
+
+# Copyright (c) 2022, 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-modules.sgml b/doc/src/sgml/archive-modules.sgml
index ef02051f7f..53e657040b 100644
--- a/doc/src/sgml/archive-modules.sgml
+++ b/doc/src/sgml/archive-modules.sgml
@@ -1,34 +1,40 @@
 <!-- doc/src/sgml/archive-modules.sgml -->
 
 <chapter id="archive-modules">
- <title>Archive Modules</title>
+ <title>Archive and Recovery Modules</title>
  <indexterm zone="archive-modules">
-  <primary>Archive Modules</primary>
+  <primary>Archive and Recovery Modules</primary>
  </indexterm>
 
  <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 (e.g., <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>
   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"/>.
+  were successfully archived.  When a custom
+  <xref linkend="guc-restore-library"/> is configured, PostgreSQL will use the
+  module for recovery actions.  It is ultimately up to the module to decide how
+  to accomplish each task, but some recommendations are listed at
+  <xref linkend="backup-archiving-wal"/> and
+  <xref linkend="backup-pitr-recovery"/>.
  </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).
+  Archive and recovery modules must at least consist of an initialization
+  function (see <xref linkend="archive-module-init"/> and
+  <xref linkend="recovery-module-init"/>) and the required callbacks (see
+  <xref linkend="archive-module-callbacks"/> and
+  <xref linkend="recovery-module-callbacks"/>).  However, archive and recovery
+  modules are also permitted to do much more (e.g., declare GUCs and register
+  background workers).  A module may be used for both
+  <varname>archive_library</varname> and <varname>restore_library</varname>.
  </para>
 
  <para>
@@ -37,7 +43,7 @@
  </para>
 
  <sect1 id="archive-module-init">
-  <title>Initialization Functions</title>
+  <title>Archive Module Initialization Functions</title>
   <indexterm zone="archive-module-init">
    <primary>_PG_archive_module_init</primary>
   </indexterm>
@@ -64,6 +70,12 @@ typedef void (*ArchiveModuleInit) (struct ArchiveModuleCallbacks *cb);
    Only the <function>archive_file_cb</function> callback is required.  The
    others are optional.
   </para>
+
+  <note>
+   <para>
+    <varname>archive_library</varname> is only loaded in the archiver process.
+   </para>
+  </note>
  </sect1>
 
  <sect1 id="archive-module-callbacks">
@@ -129,6 +141,132 @@ typedef bool (*ArchiveFileCB) (const char *file, const char *path);
 
 <programlisting>
 typedef void (*ArchiveShutdownCB) (void);
+</programlisting>
+   </para>
+  </sect2>
+ </sect1>
+
+ <sect1 id="recovery-module-init">
+  <title>Recovery Module Initialization Functions</title>
+  <indexterm zone="recovery-module-init">
+   <primary>_PG_recovery_module_init</primary>
+  </indexterm>
+  <para>
+   A recovery library is loaded by dynamically loading a shared library with the
+   <xref linkend="guc-restore-library"/> as the library base name.  The normal
+   library search path is used to locate the library.  To provide the required
+   recovery module callbacks and to indicate that the library is actually a
+   recovery module, it needs to provide a function named
+   <function>_PG_recovery_module_init</function>.  This function is passed a
+   struct that needs to be filled with the callback function pointers for
+   individual actions.
+
+<programlisting>
+typedef struct RecoveryModuleCallbacks
+{
+    RecoveryRestoreCB restore_cb;
+    RecoveryArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndCB recovery_end_cb;
+    RecoveryShutdownCB shutdown_cb;
+} RecoveryModuleCallbacks;
+typedef void (*RecoveryModuleInit) (struct RecoveryModuleCallbacks *cb);
+</programlisting>
+
+   The <function>restore_cb</function> callback is required for archive
+   recovery, but it is optional for streaming replication.  The others are
+   always optional.
+  </para>
+
+  <note>
+   <para>
+    <varname>restore_library</varname> is only loaded in the startup and
+    checkpointer processes and in single-user mode.
+   </para>
+  </note>
+ </sect1>
+
+ <sect1 id="recovery-module-callbacks">
+  <title>Recovery Module Callbacks</title>
+  <para>
+   The recovery callbacks define the actual behavior of the module.  The server
+   will call them as required to execute recovery actions.
+  </para>
+
+  <sect2 id="recovery-module-restore">
+   <title>Restore Callback</title>
+   <para>
+    The <function>restore_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 (*RecoveryRestoreCB) (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>
+  </sect2>
+
+  <sect2 id="recovery-module-archive-cleanup">
+   <title>Archive Cleanup Callback</title>
+   <para>
+    The <function>archive_cleanup_cb</function> callback is called at every
+    restart point 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 (*RecoveryArchiveCleanupCB) (const char *lastRestartPointFileName);
+</programlisting>
+
+    <replaceable>lastRestartPointFileName</replaceable> will contain the name
+    of the file containing the last valid restart point, like in
+    <link linkend="recovery-module-restore"><function>restore_cb</function></link>.
+   </para>
+  </sect2>
+
+  <sect2 id="recovery-module-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
+    replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (const char *lastRestartPointFileName);
+</programlisting>
+
+    <replaceable>lastRestartPointFileName</replaceable> will contain the name
+    of the file containing the last valid restart point, like in
+    <link linkend="recovery-module-restore"><function>restore_cb</function></link>.
+   </para>
+  </sect2>
+
+  <sect2 id="recovery-module-shutdown">
+   <title>Shutdown Callback</title>
+   <para>
+    The <function>shutdown_cb</function> callback is called when a process that
+    has loaded the recovery 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.
+
+<programlisting>
+typedef void (*RecoveryShutdownCB) (void);
 </programlisting>
    </para>
   </sect2>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index 8bab521718..d6d34078fc 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1181,9 +1181,27 @@ 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>,
-    which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>
+    that defines a restore callback, which tells
+    <productname>PostgreSQL</productname> how to retrieve archived 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,
+    recovery modules can be more performant 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="archive-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.
@@ -1202,14 +1220,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 function returns
+    <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 function provided by the
+    <varname>restore_library</varname> emits an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1233,7 +1257,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 60f23d2855..11fd670dbc 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 recovery
+  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 recovery modules.  For
+  more information about archive and recovery modules, see
+  see <xref linkend="archive-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 recovery 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,7 +65,8 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
-   Server crashes may leave temporary files with the prefix
+   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
    remove such files while the server is running as long as they are unrelated
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 77574e2d4e..039d3360ac 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3773,7 +3773,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
@@ -3801,7 +3802,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,
@@ -3836,7 +3838,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 recovery actions, including retrieving archived
+        segments of the WAL file series 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 recovery.
+        For more information, see <xref linkend="archive-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>
@@ -3881,7 +3918,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>
@@ -3910,11 +3950,13 @@ 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>
-
     </variablelist>
 
   </sect2>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index f180607528..6266e2df7f 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,9 +639,11 @@ 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
-    reaches the end of WAL available there and <varname>restore_command</varname>
-    fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s restore callback.
+    Once it reaches the end of WAL available there and
+    <varname>restore_command</varname> or the restore callback 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
     from the last valid record found in archive or <filename>pg_wal</filename>. If that fails
@@ -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 restore callbacks provided by
+     <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,10 @@ 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
+    <function>archive_cleanup_cb</function> callback function 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/shell_restore.c b/src/backend/access/transam/shell_restore.c
index 68bf6e8556..eb101b51fb 100644
--- a/src/backend/access/transam/shell_restore.c
+++ b/src/backend/access/transam/shell_restore.c
@@ -3,7 +3,8 @@
  * shell_restore.c
  *
  * These recovery 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 recovery logic.
  *
  * Copyright (c) 2022, PostgreSQL Global Development Group
  *
@@ -23,11 +24,25 @@
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static bool shell_restore(const char *file, const char *path,
+						  const char *lastRestartPointFileName);
+static void shell_archive_cleanup(const char *lastRestartPointFileName);
+static void shell_recovery_end(const char *lastRestartPointFileName);
 static bool ExecuteRecoveryCommand(const char *command,
 								   const char *commandName, bool failOnSignal,
 								   bool exitOnSigterm, uint32 wait_event_info,
 								   int fail_elevel);
 
+void
+shell_restore_init(RecoveryModuleCallbacks *cb)
+{
+	AssertVariableIsOfType(&shell_restore_init, RecoveryModuleInit);
+
+	cb->restore_cb = shell_restore;
+	cb->archive_cleanup_cb = shell_archive_cleanup;
+	cb->recovery_end_cb = shell_recovery_end;
+}
+
 bool
 shell_restore(const char *file, const char *path,
 			  const char *lastRestartPointFileName)
@@ -70,7 +85,7 @@ shell_restore(const char *file, const char *path,
 	return ret;
 }
 
-void
+static void
 shell_archive_cleanup(const char *lastRestartPointFileName)
 {
 	char	   *cmd;
@@ -83,7 +98,7 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 	pfree(cmd);
 }
 
-void
+static void
 shell_recovery_end(const char *lastRestartPointFileName)
 {
 	char	   *cmd;
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 8f47fb7570..ae537cd87f 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4884,15 +4884,16 @@ static void
 CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
+
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 */
-	if (recoveryEndCommand && strcmp(recoveryEndCommand, "") != 0)
+	if (RecoveryContext.recovery_end_cb)
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_recovery_end(lastRestartPointFname);
+		RecoveryContext.recovery_end_cb(lastRestartPointFname);
 	}
 
 	/*
@@ -7307,14 +7308,14 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Execute the archive-cleanup callback, if any.
 	 */
-	if (archiveCleanupCommand && strcmp(archiveCleanupCommand, "") != 0)
+	if (RecoveryContext.archive_cleanup_cb)
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_archive_cleanup(lastRestartPointFname);
+		RecoveryContext.archive_cleanup_cb(lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index b5cb060d55..fdab7dad43 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -22,7 +22,9 @@
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
+#include "access/xlogrecovery.h"
 #include "common/archive.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/startup.h"
@@ -32,6 +34,11 @@
 #include "storage/ipc.h"
 #include "storage/lwlock.h"
 
+/*
+ * Global context for recovery-related callbacks.
+ */
+RecoveryModuleCallbacks RecoveryContext;
+
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
  * If successful, fill "path" with its complete path (note that this will be
@@ -71,7 +78,7 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 		goto not_available;
 
 	/* In standby mode, restore_command might not be supplied */
-	if (recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0)
+	if (RecoveryContext.restore_cb == NULL)
 		goto not_available;
 
 	/*
@@ -149,14 +156,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 		XLogFileName(lastRestartPointFname, 0, 0L, wal_segment_size);
 
 	/*
-	 * Check signals before restore command and reset afterwards.
+	 * Check signals before restore callback and reset afterwards.
 	 */
 	PreRestoreCommand();
 
 	/*
 	 * Copy xlog from archival storage to XLOGDIR
 	 */
-	ret = shell_restore(xlogfname, xlogpath, lastRestartPointFname);
+	ret = RecoveryContext.restore_cb(xlogfname, xlogpath,
+									 lastRestartPointFname);
 
 	PostRestoreCommand();
 
@@ -603,3 +611,59 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the recovery callbacks into our global RecoveryContext.  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
+LoadRecoveryCallbacks(void)
+{
+	RecoveryModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_recovery_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RecoveryModuleInit)
+			load_external_function(restoreLibrary, "_PG_recovery_module_init",
+								   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("recovery modules have to define the symbol "
+						"_PG_recovery_module_init")));
+
+	memset(&RecoveryContext, 0, sizeof(RecoveryModuleCallbacks));
+	(*init) (&RecoveryContext);
+
+	/*
+	 * If using shell commands, remove callbacks for any commands that are not
+	 * set.
+	 */
+	if (restoreLibrary[0] == '\0')
+	{
+		if (recoveryRestoreCommand[0] == '\0')
+			RecoveryContext.restore_cb = NULL;
+		if (archiveCleanupCommand[0] == '\0')
+			RecoveryContext.archive_cleanup_cb = NULL;
+		if (recoveryEndCommand[0] == '\0')
+			RecoveryContext.recovery_end_cb = NULL;
+	}
+}
+
+/*
+ * Call the shutdown callback of the loaded recovery module, if defined.
+ */
+void
+call_recovery_module_shutdown_cb(int code, Datum arg)
+{
+	if (RecoveryContext.shutdown_cb)
+		RecoveryContext.shutdown_cb();
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 5e65785306..db0cd4469a 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -80,6 +80,7 @@ const struct config_enum_entry recovery_target_action_options[] = {
 
 /* options formerly taken from recovery.conf for archive recovery */
 char	   *recoveryRestoreCommand = NULL;
+char	   *restoreLibrary = NULL;
 char	   *recoveryEndCommand = NULL;
 char	   *archiveCleanupCommand = NULL;
 RecoveryTargetType recoveryTarget = RECOVERY_TARGET_UNSET;
@@ -1053,24 +1054,37 @@ validateRecoveryParameters(void)
 	if (!ArchiveRecoveryRequested)
 		return;
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryRestoreCommand, "restore_command");
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryEndCommand, "recovery_end_command");
+	before_shmem_exit(call_recovery_module_shutdown_cb, 0);
+	LoadRecoveryCallbacks();
+
 	/*
 	 * Check for compulsory parameters
 	 */
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			RecoveryContext.restore_cb == NULL)
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
-					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
+					(errmsg("specified neither primary_conninfo nor restore_command "
+							"nor a restore_library that defines a restore callback"),
+					 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 (RecoveryContext.restore_cb == NULL)
 			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 restore_library that defines "
+							"a restore callback when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index de0bbbfa79..6350fd0b83 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -38,6 +38,7 @@
 
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
+#include "access/xlogarchive.h"
 #include "access/xlogrecovery.h"
 #include "libpq/pqsignal.h"
 #include "miscadmin.h"
@@ -222,6 +223,16 @@ CheckpointerMain(void)
 	 */
 	before_shmem_exit(pgstat_before_server_shutdown, 0);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.  We do this before setting up the exception handler
+	 * so that any problems result in a server crash shortly after startup.
+	 */
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   archiveCleanupCommand, "archive_cleanup_command");
+	before_shmem_exit(call_recovery_module_shutdown_cb, 0);
+	LoadRecoveryCallbacks();
+
 	/*
 	 * Create a memory context that we will do all our work in.  We do this so
 	 * that we can reset the context during error recovery and thereby avoid
@@ -548,6 +559,9 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -563,6 +577,18 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+								   archiveCleanupCommand, "archive_cleanup_command");
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			call_recovery_module_shutdown_cb(0, (Datum) 0);
+			LoadRecoveryCallbacks();
+		}
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c
index 8ecdb9ca23..8e91f2d70f 100644
--- a/src/backend/postmaster/pgarch.c
+++ b/src/backend/postmaster/pgarch.c
@@ -831,11 +831,8 @@ 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.")));
+	CheckMutuallyExclusiveGUCs(XLogArchiveLibrary, "archive_library",
+							   XLogArchiveCommand, "archive_command");
 
 	memset(&ArchiveContext, 0, sizeof(ArchiveModuleCallbacks));
 
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index 8786186898..f9ff2b5583 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -20,6 +20,7 @@
 #include "postgres.h"
 
 #include "access/xlog.h"
+#include "access/xlogarchive.h"
 #include "access/xlogrecovery.h"
 #include "access/xlogutils.h"
 #include "libpq/pqsignal.h"
@@ -133,13 +134,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 recovery 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;
@@ -161,6 +166,22 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryRestoreCommand, "restore_command");
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryEndCommand, "recovery_end_command");
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_recovery_module_shutdown_cb(0, (Datum) 0);
+		LoadRecoveryCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index d52069f446..7858e9a649 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -6880,3 +6880,17 @@ call_enum_check_hook(struct config_enum *conf, int *newval, void **extra,
 
 	return true;
 }
+
+/*
+ * ERROR if both parameters are set.
+ */
+void
+CheckMutuallyExclusiveGUCs(const char *p1val, const char *p1name,
+						   const char *p2val, const char *p2name)
+{
+	if (p1val[0] != '\0' && p2val[0] != '\0')
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("both %s and %s set", p1name, p2name),
+				 errdetail("Only one of %s, %s may be set.", p1name, p2name)));
+}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 5025e80f89..a8a516b0c6 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -3776,6 +3776,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 recovery 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 4cceda4162..38fb3e0823 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -269,6 +269,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 recovery 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/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 59fc7bc105..756f0898b5 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -400,5 +400,6 @@ extern PGDLLIMPORT bool ArchiveRecoveryRequested;
 extern PGDLLIMPORT bool InArchiveRecovery;
 extern PGDLLIMPORT bool StandbyMode;
 extern PGDLLIMPORT char *recoveryRestoreCommand;
+extern PGDLLIMPORT char *restoreLibrary;
 
 #endif							/* XLOG_INTERNAL_H */
diff --git a/src/include/access/xlogarchive.h b/src/include/access/xlogarchive.h
index 299304703e..71c9b88165 100644
--- a/src/include/access/xlogarchive.h
+++ b/src/include/access/xlogarchive.h
@@ -30,9 +30,45 @@ extern bool XLogArchiveIsReady(const char *xlog);
 extern bool XLogArchiveIsReadyOrDone(const char *xlog);
 extern void XLogArchiveCleanup(const char *xlog);
 
-extern bool shell_restore(const char *file, const char *path,
-						  const char *lastRestartPointFileName);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Recovery module callbacks
+ *
+ * These callback functions should be defined by recovery libraries and
+ * returned via _PG_recovery_module_init().  For more information about the
+ * purpose of each callback, refer to the recovery modules documentation.
+ */
+typedef bool (*RecoveryRestoreCB) (const char *file, const char *path,
+								   const char *lastRestartPointFileName);
+typedef void (*RecoveryArchiveCleanupCB) (const char *lastRestartPointFileName);
+typedef void (*RecoveryEndCB) (const char *lastRestartPointFileName);
+typedef void (*RecoveryShutdownCB) (void);
+
+typedef struct RecoveryModuleCallbacks
+{
+	RecoveryRestoreCB restore_cb;
+	RecoveryArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndCB recovery_end_cb;
+	RecoveryShutdownCB shutdown_cb;
+} RecoveryModuleCallbacks;
+
+extern RecoveryModuleCallbacks RecoveryContext;
+
+/*
+ * Type of the shared library symbol _PG_recovery_module_init that is looked up
+ * when loading a recovery library.
+ */
+typedef void (*RecoveryModuleInit) (RecoveryModuleCallbacks *cb);
+
+extern PGDLLEXPORT void _PG_recovery_module_init(RecoveryModuleCallbacks *cb);
+
+extern void LoadRecoveryCallbacks(void);
+extern void call_recovery_module_shutdown_cb(int code, Datum arg);
+
+/*
+ * Since the logic for recovery via a shell command is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern void shell_restore_init(RecoveryModuleCallbacks *cb);
 
 #endif							/* XLOG_ARCHIVE_H */
diff --git a/src/include/access/xlogrecovery.h b/src/include/access/xlogrecovery.h
index 47c29350f5..35d1d09374 100644
--- a/src/include/access/xlogrecovery.h
+++ b/src/include/access/xlogrecovery.h
@@ -55,6 +55,7 @@ extern PGDLLIMPORT int recovery_min_apply_delay;
 extern PGDLLIMPORT char *PrimaryConnInfo;
 extern PGDLLIMPORT char *PrimarySlotName;
 extern PGDLLIMPORT char *recoveryRestoreCommand;
+extern PGDLLIMPORT char *restoreLibrary;
 extern PGDLLIMPORT char *recoveryEndCommand;
 extern PGDLLIMPORT char *archiveCleanupCommand;
 
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index ba89d013e6..947597247f 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -404,6 +404,8 @@ extern void *guc_malloc(int elevel, size_t size);
 extern pg_nodiscard void *guc_realloc(int elevel, void *old, size_t size);
 extern char *guc_strdup(int elevel, const char *src);
 extern void guc_free(void *ptr);
+extern void CheckMutuallyExclusiveGUCs(const char *p1val, const char *p1name,
+									   const char *p2val, const char *p2name);
 
 #ifdef EXEC_BACKEND
 extern void write_nondefault_variables(GucContext context);
-- 
2.25.1


--pWyiEgJYm5f9v55/--





^ permalink  raw  reply  [nested|flat] 17+ messages in thread

* [PATCH v9 1/1] Allow recovery via loadable modules.
@ 2022-12-10 03:40 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 17+ messages in thread

From: Nathan Bossart @ 2022-12-10 03:40 UTC (permalink / raw)

This adds the restore_library parameter to allow archive recovery
via a loadable module, rather than running shell commands.
---
 contrib/basic_archive/Makefile                |   4 +-
 contrib/basic_archive/basic_archive.c         |  67 ++++++-
 contrib/basic_archive/meson.build             |   7 +-
 contrib/basic_archive/t/001_restore.pl        |  44 +++++
 doc/src/sgml/archive-modules.sgml             | 168 ++++++++++++++++--
 doc/src/sgml/backup.sgml                      |  43 ++++-
 doc/src/sgml/basic-archive.sgml               |  33 ++--
 doc/src/sgml/config.sgml                      |  54 +++++-
 doc/src/sgml/high-availability.sgml           |  23 ++-
 src/backend/access/transam/shell_restore.c    |  21 ++-
 src/backend/access/transam/xlog.c             |  13 +-
 src/backend/access/transam/xlogarchive.c      |  70 +++++++-
 src/backend/access/transam/xlogrecovery.c     |  26 ++-
 src/backend/postmaster/checkpointer.c         |  26 +++
 src/backend/postmaster/pgarch.c               |   7 +-
 src/backend/postmaster/startup.c              |  23 ++-
 src/backend/utils/misc/guc.c                  |  14 ++
 src/backend/utils/misc/guc_tables.c           |  10 ++
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/access/xlog_internal.h            |   1 +
 src/include/access/xlogarchive.h              |  44 ++++-
 src/include/access/xlogrecovery.h             |   1 +
 src/include/utils/guc.h                       |   2 +
 23 files changed, 618 insertions(+), 84 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 55d299d650..487dc563f3 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -1,7 +1,7 @@
 # contrib/basic_archive/Makefile
 
 MODULES = basic_archive
-PGFILEDESC = "basic_archive - basic archive module"
+PGFILEDESC = "basic_archive - basic archive and recovery module"
 
 REGRESS = basic_archive
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.conf
@@ -9,6 +9,8 @@ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.c
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
 
+TAP_TESTS = 1
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index 3d29711a31..225a9bd3d4 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
@@ -30,6 +35,7 @@
 #include <sys/time.h>
 #include <unistd.h>
 
+#include "access/xlogarchive.h"
 #include "common/int.h"
 #include "miscadmin.h"
 #include "postmaster/pgarch.h"
@@ -48,6 +54,8 @@ static bool basic_archive_file(const char *file, const char *path);
 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 bool basic_restore_file(const char *file, const char *path,
+							   const char *lastRestartPointFileName);
 
 /*
  * _PG_init
@@ -87,6 +95,19 @@ _PG_archive_module_init(ArchiveModuleCallbacks *cb)
 	cb->archive_file_cb = basic_archive_file;
 }
 
+/*
+ * _PG_recovery_module_init
+ *
+ * Returns the module's restore callback.
+ */
+void
+_PG_recovery_module_init(RecoveryModuleCallbacks *cb)
+{
+	AssertVariableIsOfType(&_PG_recovery_module_init, RecoveryModuleInit);
+
+	cb->restore_cb = basic_restore_file;
+}
+
 /*
  * check_archive_directory
  *
@@ -99,8 +120,8 @@ check_archive_directory(char **newval, void **extra, GucSource source)
 
 	/*
 	 * The default value is an empty string, so we have to accept that value.
-	 * Our check_configured callback also checks for this and prevents
-	 * archiving from proceeding if it is still empty.
+	 * Our check_configured and restore callbacks also check for this and
+	 * prevent archiving or recovery from proceeding if it is still empty.
 	 */
 	if (*newval == NULL || *newval[0] == '\0')
 		return true;
@@ -368,3 +389,45 @@ compare_files(const char *file1, const char *file2)
 
 	return ret;
 }
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file from the WAL archives.
+ */
+static bool
+basic_restore_file(const char *file, const char *path,
+				   const char *lastRestartPointFileName)
+{
+	char		source[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive", file)));
+
+	if (archive_directory == NULL || archive_directory[0] == '\0')
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"basic_archive.archive_directory\" is not set")));
+
+	/*
+	 * Check whether the file exists.  If not, we return false to indicate that
+	 * there are no more files to restore.
+	 */
+	snprintf(source, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(source, &st) != 0)
+	{
+		int		elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m", source)));
+		return false;
+	}
+
+	copy_file(source, path);
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive", file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index bc1380e6f6..af4580dea9 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -7,7 +7,7 @@ basic_archive_sources = files(
 if host_system == 'windows'
   basic_archive_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
     '--NAME', 'basic_archive',
-    '--FILEDESC', 'basic_archive - basic archive module',])
+    '--FILEDESC', 'basic_archive - basic archive and recovery module',])
 endif
 
 basic_archive = shared_module('basic_archive',
@@ -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..ec8767d740
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,44 @@
+
+# Copyright (c) 2022, 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-modules.sgml b/doc/src/sgml/archive-modules.sgml
index ef02051f7f..53e657040b 100644
--- a/doc/src/sgml/archive-modules.sgml
+++ b/doc/src/sgml/archive-modules.sgml
@@ -1,34 +1,40 @@
 <!-- doc/src/sgml/archive-modules.sgml -->
 
 <chapter id="archive-modules">
- <title>Archive Modules</title>
+ <title>Archive and Recovery Modules</title>
  <indexterm zone="archive-modules">
-  <primary>Archive Modules</primary>
+  <primary>Archive and Recovery Modules</primary>
  </indexterm>
 
  <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 (e.g., <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>
   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"/>.
+  were successfully archived.  When a custom
+  <xref linkend="guc-restore-library"/> is configured, PostgreSQL will use the
+  module for recovery actions.  It is ultimately up to the module to decide how
+  to accomplish each task, but some recommendations are listed at
+  <xref linkend="backup-archiving-wal"/> and
+  <xref linkend="backup-pitr-recovery"/>.
  </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).
+  Archive and recovery modules must at least consist of an initialization
+  function (see <xref linkend="archive-module-init"/> and
+  <xref linkend="recovery-module-init"/>) and the required callbacks (see
+  <xref linkend="archive-module-callbacks"/> and
+  <xref linkend="recovery-module-callbacks"/>).  However, archive and recovery
+  modules are also permitted to do much more (e.g., declare GUCs and register
+  background workers).  A module may be used for both
+  <varname>archive_library</varname> and <varname>restore_library</varname>.
  </para>
 
  <para>
@@ -37,7 +43,7 @@
  </para>
 
  <sect1 id="archive-module-init">
-  <title>Initialization Functions</title>
+  <title>Archive Module Initialization Functions</title>
   <indexterm zone="archive-module-init">
    <primary>_PG_archive_module_init</primary>
   </indexterm>
@@ -64,6 +70,12 @@ typedef void (*ArchiveModuleInit) (struct ArchiveModuleCallbacks *cb);
    Only the <function>archive_file_cb</function> callback is required.  The
    others are optional.
   </para>
+
+  <note>
+   <para>
+    <varname>archive_library</varname> is only loaded in the archiver process.
+   </para>
+  </note>
  </sect1>
 
  <sect1 id="archive-module-callbacks">
@@ -129,6 +141,132 @@ typedef bool (*ArchiveFileCB) (const char *file, const char *path);
 
 <programlisting>
 typedef void (*ArchiveShutdownCB) (void);
+</programlisting>
+   </para>
+  </sect2>
+ </sect1>
+
+ <sect1 id="recovery-module-init">
+  <title>Recovery Module Initialization Functions</title>
+  <indexterm zone="recovery-module-init">
+   <primary>_PG_recovery_module_init</primary>
+  </indexterm>
+  <para>
+   A recovery library is loaded by dynamically loading a shared library with the
+   <xref linkend="guc-restore-library"/> as the library base name.  The normal
+   library search path is used to locate the library.  To provide the required
+   recovery module callbacks and to indicate that the library is actually a
+   recovery module, it needs to provide a function named
+   <function>_PG_recovery_module_init</function>.  This function is passed a
+   struct that needs to be filled with the callback function pointers for
+   individual actions.
+
+<programlisting>
+typedef struct RecoveryModuleCallbacks
+{
+    RecoveryRestoreCB restore_cb;
+    RecoveryArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndCB recovery_end_cb;
+    RecoveryShutdownCB shutdown_cb;
+} RecoveryModuleCallbacks;
+typedef void (*RecoveryModuleInit) (struct RecoveryModuleCallbacks *cb);
+</programlisting>
+
+   The <function>restore_cb</function> callback is required for archive
+   recovery, but it is optional for streaming replication.  The others are
+   always optional.
+  </para>
+
+  <note>
+   <para>
+    <varname>restore_library</varname> is only loaded in the startup and
+    checkpointer processes and in single-user mode.
+   </para>
+  </note>
+ </sect1>
+
+ <sect1 id="recovery-module-callbacks">
+  <title>Recovery Module Callbacks</title>
+  <para>
+   The recovery callbacks define the actual behavior of the module.  The server
+   will call them as required to execute recovery actions.
+  </para>
+
+  <sect2 id="recovery-module-restore">
+   <title>Restore Callback</title>
+   <para>
+    The <function>restore_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 (*RecoveryRestoreCB) (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>
+  </sect2>
+
+  <sect2 id="recovery-module-archive-cleanup">
+   <title>Archive Cleanup Callback</title>
+   <para>
+    The <function>archive_cleanup_cb</function> callback is called at every
+    restart point 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 (*RecoveryArchiveCleanupCB) (const char *lastRestartPointFileName);
+</programlisting>
+
+    <replaceable>lastRestartPointFileName</replaceable> will contain the name
+    of the file containing the last valid restart point, like in
+    <link linkend="recovery-module-restore"><function>restore_cb</function></link>.
+   </para>
+  </sect2>
+
+  <sect2 id="recovery-module-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
+    replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (const char *lastRestartPointFileName);
+</programlisting>
+
+    <replaceable>lastRestartPointFileName</replaceable> will contain the name
+    of the file containing the last valid restart point, like in
+    <link linkend="recovery-module-restore"><function>restore_cb</function></link>.
+   </para>
+  </sect2>
+
+  <sect2 id="recovery-module-shutdown">
+   <title>Shutdown Callback</title>
+   <para>
+    The <function>shutdown_cb</function> callback is called when a process that
+    has loaded the recovery 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.
+
+<programlisting>
+typedef void (*RecoveryShutdownCB) (void);
 </programlisting>
    </para>
   </sect2>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index be05a33205..f44135061d 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1180,9 +1180,27 @@ 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>,
-    which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>
+    that defines a restore callback, which tells
+    <productname>PostgreSQL</productname> how to retrieve archived 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,
+    recovery modules can be more performant 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="archive-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 +1219,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 function returns
+    <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 function provided by the
+    <varname>restore_library</varname> emits an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1232,7 +1256,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 60f23d2855..11fd670dbc 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 recovery
+  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 recovery modules.  For
+  more information about archive and recovery modules, see
+  see <xref linkend="archive-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 recovery 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,7 +65,8 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
-   Server crashes may leave temporary files with the prefix
+   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
    remove such files while the server is running as long as they are unrelated
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 89d53f2a64..ecdbfd71f7 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3773,7 +3773,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
@@ -3801,7 +3802,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,
@@ -3836,7 +3838,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 recovery actions, including retrieving archived
+        segments of the WAL file series 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 recovery.
+        For more information, see <xref linkend="archive-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>
@@ -3881,7 +3918,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>
@@ -3910,11 +3950,13 @@ 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>
-
     </variablelist>
 
   </sect2>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index f180607528..6266e2df7f 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,9 +639,11 @@ 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
-    reaches the end of WAL available there and <varname>restore_command</varname>
-    fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s restore callback.
+    Once it reaches the end of WAL available there and
+    <varname>restore_command</varname> or the restore callback 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
     from the last valid record found in archive or <filename>pg_wal</filename>. If that fails
@@ -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 restore callbacks provided by
+     <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,10 @@ 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
+    <function>archive_cleanup_cb</function> callback function 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/shell_restore.c b/src/backend/access/transam/shell_restore.c
index 8458209f49..dfd409905c 100644
--- a/src/backend/access/transam/shell_restore.c
+++ b/src/backend/access/transam/shell_restore.c
@@ -4,7 +4,8 @@
  *		Recovery functions for a user-specified shell command.
  *
  * These recovery functions use a user-specified shell command (e.g. based
- * on the GUC restore_command).
+ * on the GUC restore_command).  It is used as the default, but other
+ * modules may define their own recovery logic.
  *
  * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
@@ -24,6 +25,10 @@
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static bool shell_restore(const char *file, const char *path,
+						  const char *lastRestartPointFileName);
+static void shell_archive_cleanup(const char *lastRestartPointFileName);
+static void shell_recovery_end(const char *lastRestartPointFileName);
 static bool ExecuteRecoveryCommand(const char *command,
 								   const char *commandName,
 								   bool failOnSignal,
@@ -31,6 +36,16 @@ static bool ExecuteRecoveryCommand(const char *command,
 								   uint32 wait_event_info,
 								   int fail_elevel);
 
+void
+shell_restore_init(RecoveryModuleCallbacks *cb)
+{
+	AssertVariableIsOfType(&shell_restore_init, RecoveryModuleInit);
+
+	cb->restore_cb = shell_restore;
+	cb->archive_cleanup_cb = shell_archive_cleanup;
+	cb->recovery_end_cb = shell_recovery_end;
+}
+
 /*
  * Attempt to execute a shell-based restore command.
  *
@@ -88,7 +103,7 @@ shell_restore(const char *file, const char *path,
 /*
  * Attempt to execute a shell-based archive cleanup command.
  */
-void
+static void
 shell_archive_cleanup(const char *lastRestartPointFileName)
 {
 	char	   *cmd;
@@ -104,7 +119,7 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 /*
  * Attempt to execute a shell-based end-of-recovery command.
  */
-void
+static void
 shell_recovery_end(const char *lastRestartPointFileName)
 {
 	char	   *cmd;
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 8f47fb7570..ae537cd87f 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4884,15 +4884,16 @@ static void
 CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
+
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 */
-	if (recoveryEndCommand && strcmp(recoveryEndCommand, "") != 0)
+	if (RecoveryContext.recovery_end_cb)
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_recovery_end(lastRestartPointFname);
+		RecoveryContext.recovery_end_cb(lastRestartPointFname);
 	}
 
 	/*
@@ -7307,14 +7308,14 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Execute the archive-cleanup callback, if any.
 	 */
-	if (archiveCleanupCommand && strcmp(archiveCleanupCommand, "") != 0)
+	if (RecoveryContext.archive_cleanup_cb)
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_archive_cleanup(lastRestartPointFname);
+		RecoveryContext.archive_cleanup_cb(lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index 4b89addf97..4af5689c25 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -22,6 +22,8 @@
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
+#include "access/xlogrecovery.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/startup.h"
@@ -31,6 +33,11 @@
 #include "storage/ipc.h"
 #include "storage/lwlock.h"
 
+/*
+ * Global context for recovery-related callbacks.
+ */
+RecoveryModuleCallbacks RecoveryContext;
+
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
  * If successful, fill "path" with its complete path (note that this will be
@@ -70,7 +77,7 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 		goto not_available;
 
 	/* In standby mode, restore_command might not be supplied */
-	if (recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0)
+	if (RecoveryContext.restore_cb == NULL)
 		goto not_available;
 
 	/*
@@ -148,14 +155,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 		XLogFileName(lastRestartPointFname, 0, 0L, wal_segment_size);
 
 	/*
-	 * Check signals before restore command and reset afterwards.
+	 * Check signals before restore callback and reset afterwards.
 	 */
 	PreRestoreCommand();
 
 	/*
 	 * Copy xlog from archival storage to XLOGDIR
 	 */
-	ret = shell_restore(xlogfname, xlogpath, lastRestartPointFname);
+	ret = RecoveryContext.restore_cb(xlogfname, xlogpath,
+									 lastRestartPointFname);
 
 	PostRestoreCommand();
 
@@ -602,3 +610,59 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the recovery callbacks into our global RecoveryContext.  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
+LoadRecoveryCallbacks(void)
+{
+	RecoveryModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_recovery_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RecoveryModuleInit)
+			load_external_function(restoreLibrary, "_PG_recovery_module_init",
+								   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("recovery modules have to define the symbol "
+						"_PG_recovery_module_init")));
+
+	memset(&RecoveryContext, 0, sizeof(RecoveryModuleCallbacks));
+	(*init) (&RecoveryContext);
+
+	/*
+	 * If using shell commands, remove callbacks for any commands that are not
+	 * set.
+	 */
+	if (restoreLibrary[0] == '\0')
+	{
+		if (recoveryRestoreCommand[0] == '\0')
+			RecoveryContext.restore_cb = NULL;
+		if (archiveCleanupCommand[0] == '\0')
+			RecoveryContext.archive_cleanup_cb = NULL;
+		if (recoveryEndCommand[0] == '\0')
+			RecoveryContext.recovery_end_cb = NULL;
+	}
+}
+
+/*
+ * Call the shutdown callback of the loaded recovery module, if defined.
+ */
+void
+call_recovery_module_shutdown_cb(int code, Datum arg)
+{
+	if (RecoveryContext.shutdown_cb)
+		RecoveryContext.shutdown_cb();
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 5e65785306..db0cd4469a 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -80,6 +80,7 @@ const struct config_enum_entry recovery_target_action_options[] = {
 
 /* options formerly taken from recovery.conf for archive recovery */
 char	   *recoveryRestoreCommand = NULL;
+char	   *restoreLibrary = NULL;
 char	   *recoveryEndCommand = NULL;
 char	   *archiveCleanupCommand = NULL;
 RecoveryTargetType recoveryTarget = RECOVERY_TARGET_UNSET;
@@ -1053,24 +1054,37 @@ validateRecoveryParameters(void)
 	if (!ArchiveRecoveryRequested)
 		return;
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryRestoreCommand, "restore_command");
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryEndCommand, "recovery_end_command");
+	before_shmem_exit(call_recovery_module_shutdown_cb, 0);
+	LoadRecoveryCallbacks();
+
 	/*
 	 * Check for compulsory parameters
 	 */
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			RecoveryContext.restore_cb == NULL)
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
-					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
+					(errmsg("specified neither primary_conninfo nor restore_command "
+							"nor a restore_library that defines a restore callback"),
+					 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 (RecoveryContext.restore_cb == NULL)
 			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 restore_library that defines "
+							"a restore callback when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index de0bbbfa79..6350fd0b83 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -38,6 +38,7 @@
 
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
+#include "access/xlogarchive.h"
 #include "access/xlogrecovery.h"
 #include "libpq/pqsignal.h"
 #include "miscadmin.h"
@@ -222,6 +223,16 @@ CheckpointerMain(void)
 	 */
 	before_shmem_exit(pgstat_before_server_shutdown, 0);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.  We do this before setting up the exception handler
+	 * so that any problems result in a server crash shortly after startup.
+	 */
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   archiveCleanupCommand, "archive_cleanup_command");
+	before_shmem_exit(call_recovery_module_shutdown_cb, 0);
+	LoadRecoveryCallbacks();
+
 	/*
 	 * Create a memory context that we will do all our work in.  We do this so
 	 * that we can reset the context during error recovery and thereby avoid
@@ -548,6 +559,9 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -563,6 +577,18 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+								   archiveCleanupCommand, "archive_cleanup_command");
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			call_recovery_module_shutdown_cb(0, (Datum) 0);
+			LoadRecoveryCallbacks();
+		}
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c
index 8ecdb9ca23..8e91f2d70f 100644
--- a/src/backend/postmaster/pgarch.c
+++ b/src/backend/postmaster/pgarch.c
@@ -831,11 +831,8 @@ 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.")));
+	CheckMutuallyExclusiveGUCs(XLogArchiveLibrary, "archive_library",
+							   XLogArchiveCommand, "archive_command");
 
 	memset(&ArchiveContext, 0, sizeof(ArchiveModuleCallbacks));
 
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index 8786186898..f9ff2b5583 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -20,6 +20,7 @@
 #include "postgres.h"
 
 #include "access/xlog.h"
+#include "access/xlogarchive.h"
 #include "access/xlogrecovery.h"
 #include "access/xlogutils.h"
 #include "libpq/pqsignal.h"
@@ -133,13 +134,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 recovery 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;
@@ -161,6 +166,22 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryRestoreCommand, "restore_command");
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryEndCommand, "recovery_end_command");
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_recovery_module_shutdown_cb(0, (Datum) 0);
+		LoadRecoveryCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index d52069f446..7858e9a649 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -6880,3 +6880,17 @@ call_enum_check_hook(struct config_enum *conf, int *newval, void **extra,
 
 	return true;
 }
+
+/*
+ * ERROR if both parameters are set.
+ */
+void
+CheckMutuallyExclusiveGUCs(const char *p1val, const char *p1name,
+						   const char *p2val, const char *p2name)
+{
+	if (p1val[0] != '\0' && p2val[0] != '\0')
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("both %s and %s set", p1name, p2name),
+				 errdetail("Only one of %s, %s may be set.", p1name, p2name)));
+}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 5025e80f89..a8a516b0c6 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -3776,6 +3776,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 recovery 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 4cceda4162..38fb3e0823 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -269,6 +269,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 recovery 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/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 59fc7bc105..756f0898b5 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -400,5 +400,6 @@ extern PGDLLIMPORT bool ArchiveRecoveryRequested;
 extern PGDLLIMPORT bool InArchiveRecovery;
 extern PGDLLIMPORT bool StandbyMode;
 extern PGDLLIMPORT char *recoveryRestoreCommand;
+extern PGDLLIMPORT char *restoreLibrary;
 
 #endif							/* XLOG_INTERNAL_H */
diff --git a/src/include/access/xlogarchive.h b/src/include/access/xlogarchive.h
index 299304703e..71c9b88165 100644
--- a/src/include/access/xlogarchive.h
+++ b/src/include/access/xlogarchive.h
@@ -30,9 +30,45 @@ extern bool XLogArchiveIsReady(const char *xlog);
 extern bool XLogArchiveIsReadyOrDone(const char *xlog);
 extern void XLogArchiveCleanup(const char *xlog);
 
-extern bool shell_restore(const char *file, const char *path,
-						  const char *lastRestartPointFileName);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Recovery module callbacks
+ *
+ * These callback functions should be defined by recovery libraries and
+ * returned via _PG_recovery_module_init().  For more information about the
+ * purpose of each callback, refer to the recovery modules documentation.
+ */
+typedef bool (*RecoveryRestoreCB) (const char *file, const char *path,
+								   const char *lastRestartPointFileName);
+typedef void (*RecoveryArchiveCleanupCB) (const char *lastRestartPointFileName);
+typedef void (*RecoveryEndCB) (const char *lastRestartPointFileName);
+typedef void (*RecoveryShutdownCB) (void);
+
+typedef struct RecoveryModuleCallbacks
+{
+	RecoveryRestoreCB restore_cb;
+	RecoveryArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndCB recovery_end_cb;
+	RecoveryShutdownCB shutdown_cb;
+} RecoveryModuleCallbacks;
+
+extern RecoveryModuleCallbacks RecoveryContext;
+
+/*
+ * Type of the shared library symbol _PG_recovery_module_init that is looked up
+ * when loading a recovery library.
+ */
+typedef void (*RecoveryModuleInit) (RecoveryModuleCallbacks *cb);
+
+extern PGDLLEXPORT void _PG_recovery_module_init(RecoveryModuleCallbacks *cb);
+
+extern void LoadRecoveryCallbacks(void);
+extern void call_recovery_module_shutdown_cb(int code, Datum arg);
+
+/*
+ * Since the logic for recovery via a shell command is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern void shell_restore_init(RecoveryModuleCallbacks *cb);
 
 #endif							/* XLOG_ARCHIVE_H */
diff --git a/src/include/access/xlogrecovery.h b/src/include/access/xlogrecovery.h
index 47c29350f5..35d1d09374 100644
--- a/src/include/access/xlogrecovery.h
+++ b/src/include/access/xlogrecovery.h
@@ -55,6 +55,7 @@ extern PGDLLIMPORT int recovery_min_apply_delay;
 extern PGDLLIMPORT char *PrimaryConnInfo;
 extern PGDLLIMPORT char *PrimarySlotName;
 extern PGDLLIMPORT char *recoveryRestoreCommand;
+extern PGDLLIMPORT char *restoreLibrary;
 extern PGDLLIMPORT char *recoveryEndCommand;
 extern PGDLLIMPORT char *archiveCleanupCommand;
 
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index ba89d013e6..947597247f 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -404,6 +404,8 @@ extern void *guc_malloc(int elevel, size_t size);
 extern pg_nodiscard void *guc_realloc(int elevel, void *old, size_t size);
 extern char *guc_strdup(int elevel, const char *src);
 extern void guc_free(void *ptr);
+extern void CheckMutuallyExclusiveGUCs(const char *p1val, const char *p1name,
+									   const char *p2val, const char *p2name);
 
 #ifdef EXEC_BACKEND
 extern void write_nondefault_variables(GucContext context);
-- 
2.25.1


--1yeeQ81UyVL57Vl7--





^ permalink  raw  reply  [nested|flat] 17+ messages in thread

* [PATCH v2 3/3] Allow recovery via loadable modules.
@ 2022-12-10 03:40 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 17+ messages in thread

From: Nathan Bossart @ 2022-12-10 03:40 UTC (permalink / raw)

This adds the restore_library parameter to allow archive recovery
via a loadable module, rather than running shell commands.
---
 contrib/basic_archive/Makefile                |   4 +-
 contrib/basic_archive/basic_archive.c         |  67 ++++++-
 contrib/basic_archive/meson.build             |   7 +-
 contrib/basic_archive/t/001_restore.pl        |  42 +++++
 doc/src/sgml/archive-modules.sgml             | 168 ++++++++++++++++--
 doc/src/sgml/backup.sgml                      |  43 ++++-
 doc/src/sgml/basic-archive.sgml               |  33 ++--
 doc/src/sgml/config.sgml                      |  54 +++++-
 doc/src/sgml/high-availability.sgml           |  23 ++-
 src/backend/access/transam/shell_restore.c    |  21 ++-
 src/backend/access/transam/xlog.c             |  13 +-
 src/backend/access/transam/xlogarchive.c      |  70 +++++++-
 src/backend/access/transam/xlogrecovery.c     |  26 ++-
 src/backend/postmaster/checkpointer.c         |  26 +++
 src/backend/postmaster/pgarch.c               |   7 +-
 src/backend/postmaster/startup.c              |  23 ++-
 src/backend/utils/misc/guc.c                  |  14 ++
 src/backend/utils/misc/guc_tables.c           |  10 ++
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/access/xlog_internal.h            |   1 +
 src/include/access/xlogarchive.h              |  44 ++++-
 src/include/access/xlogrecovery.h             |   1 +
 src/include/utils/guc.h                       |   2 +
 23 files changed, 616 insertions(+), 84 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 55d299d650..487dc563f3 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -1,7 +1,7 @@
 # contrib/basic_archive/Makefile
 
 MODULES = basic_archive
-PGFILEDESC = "basic_archive - basic archive module"
+PGFILEDESC = "basic_archive - basic archive and recovery module"
 
 REGRESS = basic_archive
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.conf
@@ -9,6 +9,8 @@ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.c
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
 
+TAP_TESTS = 1
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index 9f221816bb..d6ed996f6c 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, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
@@ -30,6 +35,7 @@
 #include <sys/time.h>
 #include <unistd.h>
 
+#include "access/xlogarchive.h"
 #include "common/int.h"
 #include "miscadmin.h"
 #include "postmaster/pgarch.h"
@@ -48,6 +54,8 @@ static bool basic_archive_file(const char *file, const char *path);
 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 bool basic_restore_file(const char *file, const char *path,
+							   const char *lastRestartPointFileName);
 
 /*
  * _PG_init
@@ -87,6 +95,19 @@ _PG_archive_module_init(ArchiveModuleCallbacks *cb)
 	cb->archive_file_cb = basic_archive_file;
 }
 
+/*
+ * _PG_recovery_module_init
+ *
+ * Returns the module's restore callback.
+ */
+void
+_PG_recovery_module_init(RecoveryModuleCallbacks *cb)
+{
+	AssertVariableIsOfType(&_PG_recovery_module_init, RecoveryModuleInit);
+
+	cb->restore_cb = basic_restore_file;
+}
+
 /*
  * check_archive_directory
  *
@@ -99,8 +120,8 @@ check_archive_directory(char **newval, void **extra, GucSource source)
 
 	/*
 	 * The default value is an empty string, so we have to accept that value.
-	 * Our check_configured callback also checks for this and prevents
-	 * archiving from proceeding if it is still empty.
+	 * Our check_configured and restore callbacks also check for this and
+	 * prevent archiving or recovery from proceeding if it is still empty.
 	 */
 	if (*newval == NULL || *newval[0] == '\0')
 		return true;
@@ -368,3 +389,45 @@ compare_files(const char *file1, const char *file2)
 
 	return ret;
 }
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file from the WAL archives.
+ */
+static bool
+basic_restore_file(const char *file, const char *path,
+				   const char *lastRestartPointFileName)
+{
+	char		source[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive", file)));
+
+	if (archive_directory == NULL || archive_directory[0] == '\0')
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"basic_archive.archive_directory\" is not set")));
+
+	/*
+	 * Check whether the file exists.  If not, we return false to indicate that
+	 * there are no more files to restore.
+	 */
+	snprintf(source, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(source, &st) != 0)
+	{
+		int		elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m", source)));
+		return false;
+	}
+
+	copy_file(source, unconstify(char *, path));
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive", file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index b25dce99a3..5b5a6d79e7 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -7,7 +7,7 @@ basic_archive_sources = files(
 if host_system == 'windows'
   basic_archive_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
     '--NAME', 'basic_archive',
-    '--FILEDESC', 'basic_archive - basic archive module',])
+    '--FILEDESC', 'basic_archive - basic archive and recovery module',])
 endif
 
 basic_archive = shared_module('basic_archive',
@@ -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..86ff0dc7f5
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,42 @@
+
+# Copyright (c) 2022, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+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;
+$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-modules.sgml b/doc/src/sgml/archive-modules.sgml
index ef02051f7f..53e657040b 100644
--- a/doc/src/sgml/archive-modules.sgml
+++ b/doc/src/sgml/archive-modules.sgml
@@ -1,34 +1,40 @@
 <!-- doc/src/sgml/archive-modules.sgml -->
 
 <chapter id="archive-modules">
- <title>Archive Modules</title>
+ <title>Archive and Recovery Modules</title>
  <indexterm zone="archive-modules">
-  <primary>Archive Modules</primary>
+  <primary>Archive and Recovery Modules</primary>
  </indexterm>
 
  <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 (e.g., <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>
   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"/>.
+  were successfully archived.  When a custom
+  <xref linkend="guc-restore-library"/> is configured, PostgreSQL will use the
+  module for recovery actions.  It is ultimately up to the module to decide how
+  to accomplish each task, but some recommendations are listed at
+  <xref linkend="backup-archiving-wal"/> and
+  <xref linkend="backup-pitr-recovery"/>.
  </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).
+  Archive and recovery modules must at least consist of an initialization
+  function (see <xref linkend="archive-module-init"/> and
+  <xref linkend="recovery-module-init"/>) and the required callbacks (see
+  <xref linkend="archive-module-callbacks"/> and
+  <xref linkend="recovery-module-callbacks"/>).  However, archive and recovery
+  modules are also permitted to do much more (e.g., declare GUCs and register
+  background workers).  A module may be used for both
+  <varname>archive_library</varname> and <varname>restore_library</varname>.
  </para>
 
  <para>
@@ -37,7 +43,7 @@
  </para>
 
  <sect1 id="archive-module-init">
-  <title>Initialization Functions</title>
+  <title>Archive Module Initialization Functions</title>
   <indexterm zone="archive-module-init">
    <primary>_PG_archive_module_init</primary>
   </indexterm>
@@ -64,6 +70,12 @@ typedef void (*ArchiveModuleInit) (struct ArchiveModuleCallbacks *cb);
    Only the <function>archive_file_cb</function> callback is required.  The
    others are optional.
   </para>
+
+  <note>
+   <para>
+    <varname>archive_library</varname> is only loaded in the archiver process.
+   </para>
+  </note>
  </sect1>
 
  <sect1 id="archive-module-callbacks">
@@ -129,6 +141,132 @@ typedef bool (*ArchiveFileCB) (const char *file, const char *path);
 
 <programlisting>
 typedef void (*ArchiveShutdownCB) (void);
+</programlisting>
+   </para>
+  </sect2>
+ </sect1>
+
+ <sect1 id="recovery-module-init">
+  <title>Recovery Module Initialization Functions</title>
+  <indexterm zone="recovery-module-init">
+   <primary>_PG_recovery_module_init</primary>
+  </indexterm>
+  <para>
+   A recovery library is loaded by dynamically loading a shared library with the
+   <xref linkend="guc-restore-library"/> as the library base name.  The normal
+   library search path is used to locate the library.  To provide the required
+   recovery module callbacks and to indicate that the library is actually a
+   recovery module, it needs to provide a function named
+   <function>_PG_recovery_module_init</function>.  This function is passed a
+   struct that needs to be filled with the callback function pointers for
+   individual actions.
+
+<programlisting>
+typedef struct RecoveryModuleCallbacks
+{
+    RecoveryRestoreCB restore_cb;
+    RecoveryArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndCB recovery_end_cb;
+    RecoveryShutdownCB shutdown_cb;
+} RecoveryModuleCallbacks;
+typedef void (*RecoveryModuleInit) (struct RecoveryModuleCallbacks *cb);
+</programlisting>
+
+   The <function>restore_cb</function> callback is required for archive
+   recovery, but it is optional for streaming replication.  The others are
+   always optional.
+  </para>
+
+  <note>
+   <para>
+    <varname>restore_library</varname> is only loaded in the startup and
+    checkpointer processes and in single-user mode.
+   </para>
+  </note>
+ </sect1>
+
+ <sect1 id="recovery-module-callbacks">
+  <title>Recovery Module Callbacks</title>
+  <para>
+   The recovery callbacks define the actual behavior of the module.  The server
+   will call them as required to execute recovery actions.
+  </para>
+
+  <sect2 id="recovery-module-restore">
+   <title>Restore Callback</title>
+   <para>
+    The <function>restore_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 (*RecoveryRestoreCB) (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>
+  </sect2>
+
+  <sect2 id="recovery-module-archive-cleanup">
+   <title>Archive Cleanup Callback</title>
+   <para>
+    The <function>archive_cleanup_cb</function> callback is called at every
+    restart point 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 (*RecoveryArchiveCleanupCB) (const char *lastRestartPointFileName);
+</programlisting>
+
+    <replaceable>lastRestartPointFileName</replaceable> will contain the name
+    of the file containing the last valid restart point, like in
+    <link linkend="recovery-module-restore"><function>restore_cb</function></link>.
+   </para>
+  </sect2>
+
+  <sect2 id="recovery-module-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
+    replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (const char *lastRestartPointFileName);
+</programlisting>
+
+    <replaceable>lastRestartPointFileName</replaceable> will contain the name
+    of the file containing the last valid restart point, like in
+    <link linkend="recovery-module-restore"><function>restore_cb</function></link>.
+   </para>
+  </sect2>
+
+  <sect2 id="recovery-module-shutdown">
+   <title>Shutdown Callback</title>
+   <para>
+    The <function>shutdown_cb</function> callback is called when a process that
+    has loaded the recovery 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.
+
+<programlisting>
+typedef void (*RecoveryShutdownCB) (void);
 </programlisting>
    </para>
   </sect2>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index 8bab521718..d6d34078fc 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1181,9 +1181,27 @@ 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>,
-    which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>
+    that defines a restore callback, which tells
+    <productname>PostgreSQL</productname> how to retrieve archived 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,
+    recovery modules can be more performant 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="archive-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.
@@ -1202,14 +1220,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 function returns
+    <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 function provided by the
+    <varname>restore_library</varname> emits an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1233,7 +1257,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 0b650f17a8..ac7cd9b967 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 recovery
+  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 recovery modules.  For
+  more information about archive and recovery modules, see
+  see <xref linkend="archive-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 recovery 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>
@@ -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,7 +65,8 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
-   Server crashes may leave temporary files with the prefix
+   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
    remove such files while the server is running as long as they are unrelated
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 05b3862d09..899d2b5fdb 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3773,7 +3773,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
@@ -3801,7 +3802,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,
@@ -3836,7 +3838,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 recovery actions, including retrieving archived
+        segments of the WAL file series 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 recovery.
+        For more information, see <xref linkend="archive-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>
@@ -3881,7 +3918,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>
@@ -3910,11 +3950,13 @@ 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>
-
     </variablelist>
 
   </sect2>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index f180607528..6266e2df7f 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,9 +639,11 @@ 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
-    reaches the end of WAL available there and <varname>restore_command</varname>
-    fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s restore callback.
+    Once it reaches the end of WAL available there and
+    <varname>restore_command</varname> or the restore callback 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
     from the last valid record found in archive or <filename>pg_wal</filename>. If that fails
@@ -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 restore callbacks provided by
+     <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,10 @@ 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
+    <function>archive_cleanup_cb</function> callback function 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/shell_restore.c b/src/backend/access/transam/shell_restore.c
index 073e709e06..124a0bbdb6 100644
--- a/src/backend/access/transam/shell_restore.c
+++ b/src/backend/access/transam/shell_restore.c
@@ -3,7 +3,8 @@
  * shell_restore.c
  *
  * These recovery 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 recovery logic.
  *
  * Copyright (c) 2022, PostgreSQL Global Development Group
  *
@@ -22,6 +23,10 @@
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static bool shell_restore(const char *file, const char *path,
+						  const char *lastRestartPointFileName);
+static void shell_archive_cleanup(const char *lastRestartPointFileName);
+static void shell_recovery_end(const char *lastRestartPointFileName);
 static char *BuildCleanupCommand(const char *command,
 								 const char *lastRestartPointFileName);
 static bool ExecuteRecoveryCommand(const char *command,
@@ -29,6 +34,16 @@ static bool ExecuteRecoveryCommand(const char *command,
 								   bool exitOnSigterm, uint32 wait_event_info,
 								   int fail_elevel);
 
+void
+shell_restore_init(RecoveryModuleCallbacks *cb)
+{
+	AssertVariableIsOfType(&shell_restore_init, RecoveryModuleInit);
+
+	cb->restore_cb = shell_restore;
+	cb->archive_cleanup_cb = shell_archive_cleanup;
+	cb->recovery_end_cb = shell_recovery_end;
+}
+
 bool
 shell_restore(const char *file, const char *path,
 			  const char *lastRestartPointFileName)
@@ -73,7 +88,7 @@ shell_restore(const char *file, const char *path,
 	return ret;
 }
 
-void
+static void
 shell_archive_cleanup(const char *lastRestartPointFileName)
 {
 	char	   *cmd = BuildCleanupCommand(archiveCleanupCommand,
@@ -84,7 +99,7 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 	pfree(cmd);
 }
 
-void
+static void
 shell_recovery_end(const char *lastRestartPointFileName)
 {
 	char	   *cmd = BuildCleanupCommand(recoveryEndCommand,
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 32225be4a5..2179e44386 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4883,10 +4883,11 @@ static void
 CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
+
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 */
-	if (recoveryEndCommand && strcmp(recoveryEndCommand, "") != 0)
+	if (RecoveryContext.recovery_end_cb)
 	{
 		char		lastRestartPointFname[MAXPGPATH];
 		XLogSegNo	restartSegNo;
@@ -4903,7 +4904,7 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 		XLogFileName(lastRestartPointFname, restartTli, restartSegNo,
 					 wal_segment_size);
 
-		shell_recovery_end(lastRestartPointFname);
+		RecoveryContext.recovery_end_cb(lastRestartPointFname);
 	}
 
 	/*
@@ -7318,9 +7319,9 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Execute the archive-cleanup callback, if any.
 	 */
-	if (archiveCleanupCommand && strcmp(archiveCleanupCommand, "") != 0)
+	if (RecoveryContext.archive_cleanup_cb)
 	{
 		char		lastRestartPointFname[MAXPGPATH];
 		XLogSegNo	restartSegNo;
@@ -7337,7 +7338,7 @@ CreateRestartPoint(int flags)
 		XLogFileName(lastRestartPointFname, restartTli, restartSegNo,
 					 wal_segment_size);
 
-		shell_archive_cleanup(lastRestartPointFname);
+		RecoveryContext.archive_cleanup_cb(lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index 50b0d1105d..4caecfaea0 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -22,7 +22,9 @@
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
+#include "access/xlogrecovery.h"
 #include "common/archive.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/startup.h"
@@ -32,6 +34,11 @@
 #include "storage/ipc.h"
 #include "storage/lwlock.h"
 
+/*
+ * Global context for recovery-related callbacks.
+ */
+RecoveryModuleCallbacks RecoveryContext;
+
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
  * If successful, fill "path" with its complete path (note that this will be
@@ -71,7 +78,7 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 		goto not_available;
 
 	/* In standby mode, restore_command might not be supplied */
-	if (recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0)
+	if (RecoveryContext.restore_cb == NULL)
 		goto not_available;
 
 	/*
@@ -149,14 +156,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 		XLogFileName(lastRestartPointFname, 0, 0L, wal_segment_size);
 
 	/*
-	 * Check signals before restore command and reset afterwards.
+	 * Check signals before restore callback and reset afterwards.
 	 */
 	PreRestoreCommand();
 
 	/*
 	 * Copy xlog from archival storage to XLOGDIR
 	 */
-	ret = shell_restore(xlogfname, xlogpath, lastRestartPointFname);
+	ret = RecoveryContext.restore_cb(xlogfname, xlogpath,
+									 lastRestartPointFname);
 
 	PostRestoreCommand();
 
@@ -603,3 +611,59 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the recovery callbacks into our global RecoveryContext.  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
+LoadRecoveryCallbacks(void)
+{
+	RecoveryModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_recovery_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RecoveryModuleInit)
+			load_external_function(restoreLibrary, "_PG_recovery_module_init",
+								   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("recovery modules have to define the symbol "
+						"_PG_recovery_module_init")));
+
+	memset(&RecoveryContext, 0, sizeof(RecoveryModuleCallbacks));
+	(*init) (&RecoveryContext);
+
+	/*
+	 * If using shell commands, remove callbacks for any commands that are not
+	 * set.
+	 */
+	if (restoreLibrary[0] == '\0')
+	{
+		if (recoveryRestoreCommand[0] == '\0')
+			RecoveryContext.restore_cb = NULL;
+		if (archiveCleanupCommand[0] == '\0')
+			RecoveryContext.archive_cleanup_cb = NULL;
+		if (recoveryEndCommand[0] == '\0')
+			RecoveryContext.recovery_end_cb = NULL;
+	}
+}
+
+/*
+ * Call the shutdown callback of the loaded recovery module, if defined.
+ */
+void
+call_recovery_module_shutdown_cb(int code, Datum arg)
+{
+	if (RecoveryContext.shutdown_cb)
+		RecoveryContext.shutdown_cb();
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index d5a81f9d83..f1c0a4ffad 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -80,6 +80,7 @@ const struct config_enum_entry recovery_target_action_options[] = {
 
 /* options formerly taken from recovery.conf for archive recovery */
 char	   *recoveryRestoreCommand = NULL;
+char	   *restoreLibrary = NULL;
 char	   *recoveryEndCommand = NULL;
 char	   *archiveCleanupCommand = NULL;
 RecoveryTargetType recoveryTarget = RECOVERY_TARGET_UNSET;
@@ -1053,24 +1054,37 @@ validateRecoveryParameters(void)
 	if (!ArchiveRecoveryRequested)
 		return;
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryRestoreCommand, "restore_command");
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryEndCommand, "recovery_end_command");
+	before_shmem_exit(call_recovery_module_shutdown_cb, 0);
+	LoadRecoveryCallbacks();
+
 	/*
 	 * Check for compulsory parameters
 	 */
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			RecoveryContext.restore_cb == NULL)
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
-					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
+					(errmsg("specified neither primary_conninfo nor restore_command "
+							"nor a restore_library that defines a restore callback"),
+					 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 (RecoveryContext.restore_cb == NULL)
 			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 restore_library that defines "
+							"a restore callback when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 5fc076fc14..bfa6e4f0d0 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -38,6 +38,7 @@
 
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
+#include "access/xlogarchive.h"
 #include "access/xlogrecovery.h"
 #include "libpq/pqsignal.h"
 #include "miscadmin.h"
@@ -222,6 +223,16 @@ CheckpointerMain(void)
 	 */
 	before_shmem_exit(pgstat_before_server_shutdown, 0);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.  We do this before setting up the exception handler
+	 * so that any problems result in a server crash shortly after startup.
+	 */
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   archiveCleanupCommand, "archive_cleanup_command");
+	before_shmem_exit(call_recovery_module_shutdown_cb, 0);
+	LoadRecoveryCallbacks();
+
 	/*
 	 * Create a memory context that we will do all our work in.  We do this so
 	 * that we can reset the context during error recovery and thereby avoid
@@ -548,6 +559,9 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -563,6 +577,18 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+								   archiveCleanupCommand, "archive_cleanup_command");
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			call_recovery_module_shutdown_cb(0, (Datum) 0);
+			LoadRecoveryCallbacks();
+		}
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c
index fffb6a599c..623637d8e1 100644
--- a/src/backend/postmaster/pgarch.c
+++ b/src/backend/postmaster/pgarch.c
@@ -831,11 +831,8 @@ 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.")));
+	CheckMutuallyExclusiveGUCs(XLogArchiveLibrary, "archive_library",
+							   XLogArchiveCommand, "archive_command");
 
 	memset(&ArchiveContext, 0, sizeof(ArchiveModuleCallbacks));
 
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index f99186eab7..14701b2b85 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -20,6 +20,7 @@
 #include "postgres.h"
 
 #include "access/xlog.h"
+#include "access/xlogarchive.h"
 #include "access/xlogrecovery.h"
 #include "access/xlogutils.h"
 #include "libpq/pqsignal.h"
@@ -133,13 +134,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 recovery 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;
@@ -161,6 +166,22 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryRestoreCommand, "restore_command");
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryEndCommand, "recovery_end_command");
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_recovery_module_shutdown_cb(0, (Datum) 0);
+		LoadRecoveryCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index c6326d5053..5213b2b0b1 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -6880,3 +6880,17 @@ call_enum_check_hook(struct config_enum *conf, int *newval, void **extra,
 
 	return true;
 }
+
+/*
+ * ERROR if both parameters are set.
+ */
+void
+CheckMutuallyExclusiveGUCs(const char *p1val, const char *p1name,
+						   const char *p2val, const char *p2name)
+{
+	if (p1val[0] != '\0' && p2val[0] != '\0')
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("both %s and %s set", p1name, p2name),
+				 errdetail("Only one of %s, %s may be set.", p1name, p2name)));
+}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index a37c9f9844..8796bcf7ca 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -3764,6 +3764,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 recovery 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 5afdeb04de..e71e79271a 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -269,6 +269,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 recovery 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/access/xlog_internal.h b/src/include/access/xlog_internal.h
index e5fc66966b..133a614133 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -400,5 +400,6 @@ extern PGDLLIMPORT bool ArchiveRecoveryRequested;
 extern PGDLLIMPORT bool InArchiveRecovery;
 extern PGDLLIMPORT bool StandbyMode;
 extern PGDLLIMPORT char *recoveryRestoreCommand;
+extern PGDLLIMPORT char *restoreLibrary;
 
 #endif							/* XLOG_INTERNAL_H */
diff --git a/src/include/access/xlogarchive.h b/src/include/access/xlogarchive.h
index 69d002cdeb..090e92e554 100644
--- a/src/include/access/xlogarchive.h
+++ b/src/include/access/xlogarchive.h
@@ -30,9 +30,45 @@ extern bool XLogArchiveIsReady(const char *xlog);
 extern bool XLogArchiveIsReadyOrDone(const char *xlog);
 extern void XLogArchiveCleanup(const char *xlog);
 
-extern bool shell_restore(const char *file, const char *path,
-						  const char *lastRestartPointFileName);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Recovery module callbacks
+ *
+ * These callback functions should be defined by recovery libraries and
+ * returned via _PG_recovery_module_init().  For more information about the
+ * purpose of each callback, refer to the recovery modules documentation.
+ */
+typedef bool (*RecoveryRestoreCB) (const char *file, const char *path,
+								   const char *lastRestartPointFileName);
+typedef void (*RecoveryArchiveCleanupCB) (const char *lastRestartPointFileName);
+typedef void (*RecoveryEndCB) (const char *lastRestartPointFileName);
+typedef void (*RecoveryShutdownCB) (void);
+
+typedef struct RecoveryModuleCallbacks
+{
+	RecoveryRestoreCB restore_cb;
+	RecoveryArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndCB recovery_end_cb;
+	RecoveryShutdownCB shutdown_cb;
+} RecoveryModuleCallbacks;
+
+extern RecoveryModuleCallbacks RecoveryContext;
+
+/*
+ * Type of the shared library symbol _PG_recovery_module_init that is looked up
+ * when loading a recovery library.
+ */
+typedef void (*RecoveryModuleInit) (RecoveryModuleCallbacks *cb);
+
+extern PGDLLEXPORT void _PG_recovery_module_init(RecoveryModuleCallbacks *cb);
+
+extern void LoadRecoveryCallbacks(void);
+extern void call_recovery_module_shutdown_cb(int code, Datum arg);
+
+/*
+ * Since the logic for recovery via a shell command is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern void shell_restore_init(RecoveryModuleCallbacks *cb);
 
 #endif							/* XLOG_ARCHIVE_H */
diff --git a/src/include/access/xlogrecovery.h b/src/include/access/xlogrecovery.h
index f3398425d8..86a41ff35d 100644
--- a/src/include/access/xlogrecovery.h
+++ b/src/include/access/xlogrecovery.h
@@ -55,6 +55,7 @@ extern PGDLLIMPORT int recovery_min_apply_delay;
 extern PGDLLIMPORT char *PrimaryConnInfo;
 extern PGDLLIMPORT char *PrimarySlotName;
 extern PGDLLIMPORT char *recoveryRestoreCommand;
+extern PGDLLIMPORT char *restoreLibrary;
 extern PGDLLIMPORT char *recoveryEndCommand;
 extern PGDLLIMPORT char *archiveCleanupCommand;
 
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index 91cc341854..d1445cdfcb 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -404,6 +404,8 @@ extern void *guc_malloc(int elevel, size_t size);
 extern pg_nodiscard void *guc_realloc(int elevel, void *old, size_t size);
 extern char *guc_strdup(int elevel, const char *src);
 extern void guc_free(void *ptr);
+extern void CheckMutuallyExclusiveGUCs(const char *p1val, const char *p1name,
+									   const char *p2val, const char *p2name);
 
 #ifdef EXEC_BACKEND
 extern void write_nondefault_variables(GucContext context);
-- 
2.25.1


--vkogqOf2sHV7VnPd--





^ permalink  raw  reply  [nested|flat] 17+ messages in thread

* [PATCH v3 3/3] Allow recovery via loadable modules.
@ 2022-12-10 03:40 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 17+ messages in thread

From: Nathan Bossart @ 2022-12-10 03:40 UTC (permalink / raw)

This adds the restore_library parameter to allow archive recovery
via a loadable module, rather than running shell commands.
---
 contrib/basic_archive/Makefile                |   4 +-
 contrib/basic_archive/basic_archive.c         |  67 ++++++-
 contrib/basic_archive/meson.build             |   7 +-
 contrib/basic_archive/t/001_restore.pl        |  42 +++++
 doc/src/sgml/archive-modules.sgml             | 168 ++++++++++++++++--
 doc/src/sgml/backup.sgml                      |  43 ++++-
 doc/src/sgml/basic-archive.sgml               |  33 ++--
 doc/src/sgml/config.sgml                      |  54 +++++-
 doc/src/sgml/high-availability.sgml           |  23 ++-
 src/backend/access/transam/shell_restore.c    |  21 ++-
 src/backend/access/transam/xlog.c             |  13 +-
 src/backend/access/transam/xlogarchive.c      |  70 +++++++-
 src/backend/access/transam/xlogrecovery.c     |  26 ++-
 src/backend/postmaster/checkpointer.c         |  26 +++
 src/backend/postmaster/pgarch.c               |   7 +-
 src/backend/postmaster/startup.c              |  23 ++-
 src/backend/utils/misc/guc.c                  |  14 ++
 src/backend/utils/misc/guc_tables.c           |  10 ++
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/access/xlog_internal.h            |   1 +
 src/include/access/xlogarchive.h              |  44 ++++-
 src/include/access/xlogrecovery.h             |   1 +
 src/include/utils/guc.h                       |   2 +
 23 files changed, 616 insertions(+), 84 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 55d299d650..487dc563f3 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -1,7 +1,7 @@
 # contrib/basic_archive/Makefile
 
 MODULES = basic_archive
-PGFILEDESC = "basic_archive - basic archive module"
+PGFILEDESC = "basic_archive - basic archive and recovery module"
 
 REGRESS = basic_archive
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.conf
@@ -9,6 +9,8 @@ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.c
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
 
+TAP_TESTS = 1
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index 28cbb6cce0..8c333c8f99 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
@@ -30,6 +35,7 @@
 #include <sys/time.h>
 #include <unistd.h>
 
+#include "access/xlogarchive.h"
 #include "common/int.h"
 #include "miscadmin.h"
 #include "postmaster/pgarch.h"
@@ -48,6 +54,8 @@ static bool basic_archive_file(const char *file, const char *path);
 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 bool basic_restore_file(const char *file, const char *path,
+							   const char *lastRestartPointFileName);
 
 /*
  * _PG_init
@@ -87,6 +95,19 @@ _PG_archive_module_init(ArchiveModuleCallbacks *cb)
 	cb->archive_file_cb = basic_archive_file;
 }
 
+/*
+ * _PG_recovery_module_init
+ *
+ * Returns the module's restore callback.
+ */
+void
+_PG_recovery_module_init(RecoveryModuleCallbacks *cb)
+{
+	AssertVariableIsOfType(&_PG_recovery_module_init, RecoveryModuleInit);
+
+	cb->restore_cb = basic_restore_file;
+}
+
 /*
  * check_archive_directory
  *
@@ -99,8 +120,8 @@ check_archive_directory(char **newval, void **extra, GucSource source)
 
 	/*
 	 * The default value is an empty string, so we have to accept that value.
-	 * Our check_configured callback also checks for this and prevents
-	 * archiving from proceeding if it is still empty.
+	 * Our check_configured and restore callbacks also check for this and
+	 * prevent archiving or recovery from proceeding if it is still empty.
 	 */
 	if (*newval == NULL || *newval[0] == '\0')
 		return true;
@@ -368,3 +389,45 @@ compare_files(const char *file1, const char *file2)
 
 	return ret;
 }
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file from the WAL archives.
+ */
+static bool
+basic_restore_file(const char *file, const char *path,
+				   const char *lastRestartPointFileName)
+{
+	char		source[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive", file)));
+
+	if (archive_directory == NULL || archive_directory[0] == '\0')
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"basic_archive.archive_directory\" is not set")));
+
+	/*
+	 * Check whether the file exists.  If not, we return false to indicate that
+	 * there are no more files to restore.
+	 */
+	snprintf(source, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(source, &st) != 0)
+	{
+		int		elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m", source)));
+		return false;
+	}
+
+	copy_file(source, unconstify(char *, path));
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive", file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index bc1380e6f6..af4580dea9 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -7,7 +7,7 @@ basic_archive_sources = files(
 if host_system == 'windows'
   basic_archive_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
     '--NAME', 'basic_archive',
-    '--FILEDESC', 'basic_archive - basic archive module',])
+    '--FILEDESC', 'basic_archive - basic archive and recovery module',])
 endif
 
 basic_archive = shared_module('basic_archive',
@@ -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..86ff0dc7f5
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,42 @@
+
+# Copyright (c) 2022, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+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;
+$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-modules.sgml b/doc/src/sgml/archive-modules.sgml
index ef02051f7f..53e657040b 100644
--- a/doc/src/sgml/archive-modules.sgml
+++ b/doc/src/sgml/archive-modules.sgml
@@ -1,34 +1,40 @@
 <!-- doc/src/sgml/archive-modules.sgml -->
 
 <chapter id="archive-modules">
- <title>Archive Modules</title>
+ <title>Archive and Recovery Modules</title>
  <indexterm zone="archive-modules">
-  <primary>Archive Modules</primary>
+  <primary>Archive and Recovery Modules</primary>
  </indexterm>
 
  <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 (e.g., <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>
   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"/>.
+  were successfully archived.  When a custom
+  <xref linkend="guc-restore-library"/> is configured, PostgreSQL will use the
+  module for recovery actions.  It is ultimately up to the module to decide how
+  to accomplish each task, but some recommendations are listed at
+  <xref linkend="backup-archiving-wal"/> and
+  <xref linkend="backup-pitr-recovery"/>.
  </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).
+  Archive and recovery modules must at least consist of an initialization
+  function (see <xref linkend="archive-module-init"/> and
+  <xref linkend="recovery-module-init"/>) and the required callbacks (see
+  <xref linkend="archive-module-callbacks"/> and
+  <xref linkend="recovery-module-callbacks"/>).  However, archive and recovery
+  modules are also permitted to do much more (e.g., declare GUCs and register
+  background workers).  A module may be used for both
+  <varname>archive_library</varname> and <varname>restore_library</varname>.
  </para>
 
  <para>
@@ -37,7 +43,7 @@
  </para>
 
  <sect1 id="archive-module-init">
-  <title>Initialization Functions</title>
+  <title>Archive Module Initialization Functions</title>
   <indexterm zone="archive-module-init">
    <primary>_PG_archive_module_init</primary>
   </indexterm>
@@ -64,6 +70,12 @@ typedef void (*ArchiveModuleInit) (struct ArchiveModuleCallbacks *cb);
    Only the <function>archive_file_cb</function> callback is required.  The
    others are optional.
   </para>
+
+  <note>
+   <para>
+    <varname>archive_library</varname> is only loaded in the archiver process.
+   </para>
+  </note>
  </sect1>
 
  <sect1 id="archive-module-callbacks">
@@ -129,6 +141,132 @@ typedef bool (*ArchiveFileCB) (const char *file, const char *path);
 
 <programlisting>
 typedef void (*ArchiveShutdownCB) (void);
+</programlisting>
+   </para>
+  </sect2>
+ </sect1>
+
+ <sect1 id="recovery-module-init">
+  <title>Recovery Module Initialization Functions</title>
+  <indexterm zone="recovery-module-init">
+   <primary>_PG_recovery_module_init</primary>
+  </indexterm>
+  <para>
+   A recovery library is loaded by dynamically loading a shared library with the
+   <xref linkend="guc-restore-library"/> as the library base name.  The normal
+   library search path is used to locate the library.  To provide the required
+   recovery module callbacks and to indicate that the library is actually a
+   recovery module, it needs to provide a function named
+   <function>_PG_recovery_module_init</function>.  This function is passed a
+   struct that needs to be filled with the callback function pointers for
+   individual actions.
+
+<programlisting>
+typedef struct RecoveryModuleCallbacks
+{
+    RecoveryRestoreCB restore_cb;
+    RecoveryArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndCB recovery_end_cb;
+    RecoveryShutdownCB shutdown_cb;
+} RecoveryModuleCallbacks;
+typedef void (*RecoveryModuleInit) (struct RecoveryModuleCallbacks *cb);
+</programlisting>
+
+   The <function>restore_cb</function> callback is required for archive
+   recovery, but it is optional for streaming replication.  The others are
+   always optional.
+  </para>
+
+  <note>
+   <para>
+    <varname>restore_library</varname> is only loaded in the startup and
+    checkpointer processes and in single-user mode.
+   </para>
+  </note>
+ </sect1>
+
+ <sect1 id="recovery-module-callbacks">
+  <title>Recovery Module Callbacks</title>
+  <para>
+   The recovery callbacks define the actual behavior of the module.  The server
+   will call them as required to execute recovery actions.
+  </para>
+
+  <sect2 id="recovery-module-restore">
+   <title>Restore Callback</title>
+   <para>
+    The <function>restore_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 (*RecoveryRestoreCB) (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>
+  </sect2>
+
+  <sect2 id="recovery-module-archive-cleanup">
+   <title>Archive Cleanup Callback</title>
+   <para>
+    The <function>archive_cleanup_cb</function> callback is called at every
+    restart point 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 (*RecoveryArchiveCleanupCB) (const char *lastRestartPointFileName);
+</programlisting>
+
+    <replaceable>lastRestartPointFileName</replaceable> will contain the name
+    of the file containing the last valid restart point, like in
+    <link linkend="recovery-module-restore"><function>restore_cb</function></link>.
+   </para>
+  </sect2>
+
+  <sect2 id="recovery-module-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
+    replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (const char *lastRestartPointFileName);
+</programlisting>
+
+    <replaceable>lastRestartPointFileName</replaceable> will contain the name
+    of the file containing the last valid restart point, like in
+    <link linkend="recovery-module-restore"><function>restore_cb</function></link>.
+   </para>
+  </sect2>
+
+  <sect2 id="recovery-module-shutdown">
+   <title>Shutdown Callback</title>
+   <para>
+    The <function>shutdown_cb</function> callback is called when a process that
+    has loaded the recovery 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.
+
+<programlisting>
+typedef void (*RecoveryShutdownCB) (void);
 </programlisting>
    </para>
   </sect2>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index 8bab521718..d6d34078fc 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1181,9 +1181,27 @@ 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>,
-    which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>
+    that defines a restore callback, which tells
+    <productname>PostgreSQL</productname> how to retrieve archived 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,
+    recovery modules can be more performant 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="archive-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.
@@ -1202,14 +1220,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 function returns
+    <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 function provided by the
+    <varname>restore_library</varname> emits an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1233,7 +1257,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 0b650f17a8..ac7cd9b967 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 recovery
+  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 recovery modules.  For
+  more information about archive and recovery modules, see
+  see <xref linkend="archive-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 recovery 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>
@@ -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,7 +65,8 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
-   Server crashes may leave temporary files with the prefix
+   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
    remove such files while the server is running as long as they are unrelated
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 05b3862d09..899d2b5fdb 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3773,7 +3773,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
@@ -3801,7 +3802,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,
@@ -3836,7 +3838,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 recovery actions, including retrieving archived
+        segments of the WAL file series 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 recovery.
+        For more information, see <xref linkend="archive-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>
@@ -3881,7 +3918,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>
@@ -3910,11 +3950,13 @@ 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>
-
     </variablelist>
 
   </sect2>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index f180607528..6266e2df7f 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,9 +639,11 @@ 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
-    reaches the end of WAL available there and <varname>restore_command</varname>
-    fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s restore callback.
+    Once it reaches the end of WAL available there and
+    <varname>restore_command</varname> or the restore callback 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
     from the last valid record found in archive or <filename>pg_wal</filename>. If that fails
@@ -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 restore callbacks provided by
+     <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,10 @@ 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
+    <function>archive_cleanup_cb</function> callback function 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/shell_restore.c b/src/backend/access/transam/shell_restore.c
index 073e709e06..124a0bbdb6 100644
--- a/src/backend/access/transam/shell_restore.c
+++ b/src/backend/access/transam/shell_restore.c
@@ -3,7 +3,8 @@
  * shell_restore.c
  *
  * These recovery 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 recovery logic.
  *
  * Copyright (c) 2022, PostgreSQL Global Development Group
  *
@@ -22,6 +23,10 @@
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static bool shell_restore(const char *file, const char *path,
+						  const char *lastRestartPointFileName);
+static void shell_archive_cleanup(const char *lastRestartPointFileName);
+static void shell_recovery_end(const char *lastRestartPointFileName);
 static char *BuildCleanupCommand(const char *command,
 								 const char *lastRestartPointFileName);
 static bool ExecuteRecoveryCommand(const char *command,
@@ -29,6 +34,16 @@ static bool ExecuteRecoveryCommand(const char *command,
 								   bool exitOnSigterm, uint32 wait_event_info,
 								   int fail_elevel);
 
+void
+shell_restore_init(RecoveryModuleCallbacks *cb)
+{
+	AssertVariableIsOfType(&shell_restore_init, RecoveryModuleInit);
+
+	cb->restore_cb = shell_restore;
+	cb->archive_cleanup_cb = shell_archive_cleanup;
+	cb->recovery_end_cb = shell_recovery_end;
+}
+
 bool
 shell_restore(const char *file, const char *path,
 			  const char *lastRestartPointFileName)
@@ -73,7 +88,7 @@ shell_restore(const char *file, const char *path,
 	return ret;
 }
 
-void
+static void
 shell_archive_cleanup(const char *lastRestartPointFileName)
 {
 	char	   *cmd = BuildCleanupCommand(archiveCleanupCommand,
@@ -84,7 +99,7 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 	pfree(cmd);
 }
 
-void
+static void
 shell_recovery_end(const char *lastRestartPointFileName)
 {
 	char	   *cmd = BuildCleanupCommand(recoveryEndCommand,
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index fdce12614a..c4ad571327 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4883,10 +4883,11 @@ static void
 CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
+
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 */
-	if (recoveryEndCommand && strcmp(recoveryEndCommand, "") != 0)
+	if (RecoveryContext.recovery_end_cb)
 	{
 		char		lastRestartPointFname[MAXPGPATH];
 		XLogSegNo	restartSegNo;
@@ -4903,7 +4904,7 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 		XLogFileName(lastRestartPointFname, restartTli, restartSegNo,
 					 wal_segment_size);
 
-		shell_recovery_end(lastRestartPointFname);
+		RecoveryContext.recovery_end_cb(lastRestartPointFname);
 	}
 
 	/*
@@ -7318,9 +7319,9 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Execute the archive-cleanup callback, if any.
 	 */
-	if (archiveCleanupCommand && strcmp(archiveCleanupCommand, "") != 0)
+	if (RecoveryContext.archive_cleanup_cb)
 	{
 		char		lastRestartPointFname[MAXPGPATH];
 		XLogSegNo	restartSegNo;
@@ -7337,7 +7338,7 @@ CreateRestartPoint(int flags)
 		XLogFileName(lastRestartPointFname, restartTli, restartSegNo,
 					 wal_segment_size);
 
-		shell_archive_cleanup(lastRestartPointFname);
+		RecoveryContext.archive_cleanup_cb(lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index b5cb060d55..fdab7dad43 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -22,7 +22,9 @@
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
+#include "access/xlogrecovery.h"
 #include "common/archive.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/startup.h"
@@ -32,6 +34,11 @@
 #include "storage/ipc.h"
 #include "storage/lwlock.h"
 
+/*
+ * Global context for recovery-related callbacks.
+ */
+RecoveryModuleCallbacks RecoveryContext;
+
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
  * If successful, fill "path" with its complete path (note that this will be
@@ -71,7 +78,7 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 		goto not_available;
 
 	/* In standby mode, restore_command might not be supplied */
-	if (recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0)
+	if (RecoveryContext.restore_cb == NULL)
 		goto not_available;
 
 	/*
@@ -149,14 +156,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 		XLogFileName(lastRestartPointFname, 0, 0L, wal_segment_size);
 
 	/*
-	 * Check signals before restore command and reset afterwards.
+	 * Check signals before restore callback and reset afterwards.
 	 */
 	PreRestoreCommand();
 
 	/*
 	 * Copy xlog from archival storage to XLOGDIR
 	 */
-	ret = shell_restore(xlogfname, xlogpath, lastRestartPointFname);
+	ret = RecoveryContext.restore_cb(xlogfname, xlogpath,
+									 lastRestartPointFname);
 
 	PostRestoreCommand();
 
@@ -603,3 +611,59 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the recovery callbacks into our global RecoveryContext.  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
+LoadRecoveryCallbacks(void)
+{
+	RecoveryModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_recovery_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RecoveryModuleInit)
+			load_external_function(restoreLibrary, "_PG_recovery_module_init",
+								   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("recovery modules have to define the symbol "
+						"_PG_recovery_module_init")));
+
+	memset(&RecoveryContext, 0, sizeof(RecoveryModuleCallbacks));
+	(*init) (&RecoveryContext);
+
+	/*
+	 * If using shell commands, remove callbacks for any commands that are not
+	 * set.
+	 */
+	if (restoreLibrary[0] == '\0')
+	{
+		if (recoveryRestoreCommand[0] == '\0')
+			RecoveryContext.restore_cb = NULL;
+		if (archiveCleanupCommand[0] == '\0')
+			RecoveryContext.archive_cleanup_cb = NULL;
+		if (recoveryEndCommand[0] == '\0')
+			RecoveryContext.recovery_end_cb = NULL;
+	}
+}
+
+/*
+ * Call the shutdown callback of the loaded recovery module, if defined.
+ */
+void
+call_recovery_module_shutdown_cb(int code, Datum arg)
+{
+	if (RecoveryContext.shutdown_cb)
+		RecoveryContext.shutdown_cb();
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index bc3c3eb3e7..95bcc3a0b7 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -80,6 +80,7 @@ const struct config_enum_entry recovery_target_action_options[] = {
 
 /* options formerly taken from recovery.conf for archive recovery */
 char	   *recoveryRestoreCommand = NULL;
+char	   *restoreLibrary = NULL;
 char	   *recoveryEndCommand = NULL;
 char	   *archiveCleanupCommand = NULL;
 RecoveryTargetType recoveryTarget = RECOVERY_TARGET_UNSET;
@@ -1053,24 +1054,37 @@ validateRecoveryParameters(void)
 	if (!ArchiveRecoveryRequested)
 		return;
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryRestoreCommand, "restore_command");
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryEndCommand, "recovery_end_command");
+	before_shmem_exit(call_recovery_module_shutdown_cb, 0);
+	LoadRecoveryCallbacks();
+
 	/*
 	 * Check for compulsory parameters
 	 */
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			RecoveryContext.restore_cb == NULL)
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
-					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
+					(errmsg("specified neither primary_conninfo nor restore_command "
+							"nor a restore_library that defines a restore callback"),
+					 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 (RecoveryContext.restore_cb == NULL)
 			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 restore_library that defines "
+							"a restore callback when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index de0bbbfa79..6350fd0b83 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -38,6 +38,7 @@
 
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
+#include "access/xlogarchive.h"
 #include "access/xlogrecovery.h"
 #include "libpq/pqsignal.h"
 #include "miscadmin.h"
@@ -222,6 +223,16 @@ CheckpointerMain(void)
 	 */
 	before_shmem_exit(pgstat_before_server_shutdown, 0);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.  We do this before setting up the exception handler
+	 * so that any problems result in a server crash shortly after startup.
+	 */
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   archiveCleanupCommand, "archive_cleanup_command");
+	before_shmem_exit(call_recovery_module_shutdown_cb, 0);
+	LoadRecoveryCallbacks();
+
 	/*
 	 * Create a memory context that we will do all our work in.  We do this so
 	 * that we can reset the context during error recovery and thereby avoid
@@ -548,6 +559,9 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -563,6 +577,18 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+								   archiveCleanupCommand, "archive_cleanup_command");
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			call_recovery_module_shutdown_cb(0, (Datum) 0);
+			LoadRecoveryCallbacks();
+		}
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c
index 8ecdb9ca23..8e91f2d70f 100644
--- a/src/backend/postmaster/pgarch.c
+++ b/src/backend/postmaster/pgarch.c
@@ -831,11 +831,8 @@ 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.")));
+	CheckMutuallyExclusiveGUCs(XLogArchiveLibrary, "archive_library",
+							   XLogArchiveCommand, "archive_command");
 
 	memset(&ArchiveContext, 0, sizeof(ArchiveModuleCallbacks));
 
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index 8786186898..f9ff2b5583 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -20,6 +20,7 @@
 #include "postgres.h"
 
 #include "access/xlog.h"
+#include "access/xlogarchive.h"
 #include "access/xlogrecovery.h"
 #include "access/xlogutils.h"
 #include "libpq/pqsignal.h"
@@ -133,13 +134,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 recovery 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;
@@ -161,6 +166,22 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryRestoreCommand, "restore_command");
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryEndCommand, "recovery_end_command");
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_recovery_module_shutdown_cb(0, (Datum) 0);
+		LoadRecoveryCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index d52069f446..7858e9a649 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -6880,3 +6880,17 @@ call_enum_check_hook(struct config_enum *conf, int *newval, void **extra,
 
 	return true;
 }
+
+/*
+ * ERROR if both parameters are set.
+ */
+void
+CheckMutuallyExclusiveGUCs(const char *p1val, const char *p1name,
+						   const char *p2val, const char *p2name)
+{
+	if (p1val[0] != '\0' && p2val[0] != '\0')
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("both %s and %s set", p1name, p2name),
+				 errdetail("Only one of %s, %s may be set.", p1name, p2name)));
+}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 68328b1402..19746e2489 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -3764,6 +3764,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 recovery 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 5afdeb04de..e71e79271a 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -269,6 +269,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 recovery 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/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 59fc7bc105..756f0898b5 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -400,5 +400,6 @@ extern PGDLLIMPORT bool ArchiveRecoveryRequested;
 extern PGDLLIMPORT bool InArchiveRecovery;
 extern PGDLLIMPORT bool StandbyMode;
 extern PGDLLIMPORT char *recoveryRestoreCommand;
+extern PGDLLIMPORT char *restoreLibrary;
 
 #endif							/* XLOG_INTERNAL_H */
diff --git a/src/include/access/xlogarchive.h b/src/include/access/xlogarchive.h
index 299304703e..71c9b88165 100644
--- a/src/include/access/xlogarchive.h
+++ b/src/include/access/xlogarchive.h
@@ -30,9 +30,45 @@ extern bool XLogArchiveIsReady(const char *xlog);
 extern bool XLogArchiveIsReadyOrDone(const char *xlog);
 extern void XLogArchiveCleanup(const char *xlog);
 
-extern bool shell_restore(const char *file, const char *path,
-						  const char *lastRestartPointFileName);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Recovery module callbacks
+ *
+ * These callback functions should be defined by recovery libraries and
+ * returned via _PG_recovery_module_init().  For more information about the
+ * purpose of each callback, refer to the recovery modules documentation.
+ */
+typedef bool (*RecoveryRestoreCB) (const char *file, const char *path,
+								   const char *lastRestartPointFileName);
+typedef void (*RecoveryArchiveCleanupCB) (const char *lastRestartPointFileName);
+typedef void (*RecoveryEndCB) (const char *lastRestartPointFileName);
+typedef void (*RecoveryShutdownCB) (void);
+
+typedef struct RecoveryModuleCallbacks
+{
+	RecoveryRestoreCB restore_cb;
+	RecoveryArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndCB recovery_end_cb;
+	RecoveryShutdownCB shutdown_cb;
+} RecoveryModuleCallbacks;
+
+extern RecoveryModuleCallbacks RecoveryContext;
+
+/*
+ * Type of the shared library symbol _PG_recovery_module_init that is looked up
+ * when loading a recovery library.
+ */
+typedef void (*RecoveryModuleInit) (RecoveryModuleCallbacks *cb);
+
+extern PGDLLEXPORT void _PG_recovery_module_init(RecoveryModuleCallbacks *cb);
+
+extern void LoadRecoveryCallbacks(void);
+extern void call_recovery_module_shutdown_cb(int code, Datum arg);
+
+/*
+ * Since the logic for recovery via a shell command is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern void shell_restore_init(RecoveryModuleCallbacks *cb);
 
 #endif							/* XLOG_ARCHIVE_H */
diff --git a/src/include/access/xlogrecovery.h b/src/include/access/xlogrecovery.h
index 47c29350f5..35d1d09374 100644
--- a/src/include/access/xlogrecovery.h
+++ b/src/include/access/xlogrecovery.h
@@ -55,6 +55,7 @@ extern PGDLLIMPORT int recovery_min_apply_delay;
 extern PGDLLIMPORT char *PrimaryConnInfo;
 extern PGDLLIMPORT char *PrimarySlotName;
 extern PGDLLIMPORT char *recoveryRestoreCommand;
+extern PGDLLIMPORT char *restoreLibrary;
 extern PGDLLIMPORT char *recoveryEndCommand;
 extern PGDLLIMPORT char *archiveCleanupCommand;
 
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index ba89d013e6..947597247f 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -404,6 +404,8 @@ extern void *guc_malloc(int elevel, size_t size);
 extern pg_nodiscard void *guc_realloc(int elevel, void *old, size_t size);
 extern char *guc_strdup(int elevel, const char *src);
 extern void guc_free(void *ptr);
+extern void CheckMutuallyExclusiveGUCs(const char *p1val, const char *p1name,
+									   const char *p2val, const char *p2name);
 
 #ifdef EXEC_BACKEND
 extern void write_nondefault_variables(GucContext context);
-- 
2.25.1


--KsGdsel6WgEHnImy--





^ permalink  raw  reply  [nested|flat] 17+ messages in thread

* [PATCH v5 3/3] Allow recovery via loadable modules.
@ 2022-12-10 03:40 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 17+ messages in thread

From: Nathan Bossart @ 2022-12-10 03:40 UTC (permalink / raw)

This adds the restore_library parameter to allow archive recovery
via a loadable module, rather than running shell commands.
---
 contrib/basic_archive/Makefile                |   4 +-
 contrib/basic_archive/basic_archive.c         |  67 ++++++-
 contrib/basic_archive/meson.build             |   7 +-
 contrib/basic_archive/t/001_restore.pl        |  44 +++++
 doc/src/sgml/archive-modules.sgml             | 168 ++++++++++++++++--
 doc/src/sgml/backup.sgml                      |  43 ++++-
 doc/src/sgml/basic-archive.sgml               |  33 ++--
 doc/src/sgml/config.sgml                      |  54 +++++-
 doc/src/sgml/high-availability.sgml           |  23 ++-
 src/backend/access/transam/shell_restore.c    |  21 ++-
 src/backend/access/transam/xlog.c             |  13 +-
 src/backend/access/transam/xlogarchive.c      |  70 +++++++-
 src/backend/access/transam/xlogrecovery.c     |  26 ++-
 src/backend/postmaster/checkpointer.c         |  26 +++
 src/backend/postmaster/pgarch.c               |   7 +-
 src/backend/postmaster/startup.c              |  23 ++-
 src/backend/utils/misc/guc.c                  |  14 ++
 src/backend/utils/misc/guc_tables.c           |  10 ++
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/access/xlog_internal.h            |   1 +
 src/include/access/xlogarchive.h              |  44 ++++-
 src/include/access/xlogrecovery.h             |   1 +
 src/include/utils/guc.h                       |   2 +
 23 files changed, 618 insertions(+), 84 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 55d299d650..487dc563f3 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -1,7 +1,7 @@
 # contrib/basic_archive/Makefile
 
 MODULES = basic_archive
-PGFILEDESC = "basic_archive - basic archive module"
+PGFILEDESC = "basic_archive - basic archive and recovery module"
 
 REGRESS = basic_archive
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.conf
@@ -9,6 +9,8 @@ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.c
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
 
+TAP_TESTS = 1
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index 28cbb6cce0..8c333c8f99 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
@@ -30,6 +35,7 @@
 #include <sys/time.h>
 #include <unistd.h>
 
+#include "access/xlogarchive.h"
 #include "common/int.h"
 #include "miscadmin.h"
 #include "postmaster/pgarch.h"
@@ -48,6 +54,8 @@ static bool basic_archive_file(const char *file, const char *path);
 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 bool basic_restore_file(const char *file, const char *path,
+							   const char *lastRestartPointFileName);
 
 /*
  * _PG_init
@@ -87,6 +95,19 @@ _PG_archive_module_init(ArchiveModuleCallbacks *cb)
 	cb->archive_file_cb = basic_archive_file;
 }
 
+/*
+ * _PG_recovery_module_init
+ *
+ * Returns the module's restore callback.
+ */
+void
+_PG_recovery_module_init(RecoveryModuleCallbacks *cb)
+{
+	AssertVariableIsOfType(&_PG_recovery_module_init, RecoveryModuleInit);
+
+	cb->restore_cb = basic_restore_file;
+}
+
 /*
  * check_archive_directory
  *
@@ -99,8 +120,8 @@ check_archive_directory(char **newval, void **extra, GucSource source)
 
 	/*
 	 * The default value is an empty string, so we have to accept that value.
-	 * Our check_configured callback also checks for this and prevents
-	 * archiving from proceeding if it is still empty.
+	 * Our check_configured and restore callbacks also check for this and
+	 * prevent archiving or recovery from proceeding if it is still empty.
 	 */
 	if (*newval == NULL || *newval[0] == '\0')
 		return true;
@@ -368,3 +389,45 @@ compare_files(const char *file1, const char *file2)
 
 	return ret;
 }
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file from the WAL archives.
+ */
+static bool
+basic_restore_file(const char *file, const char *path,
+				   const char *lastRestartPointFileName)
+{
+	char		source[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive", file)));
+
+	if (archive_directory == NULL || archive_directory[0] == '\0')
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"basic_archive.archive_directory\" is not set")));
+
+	/*
+	 * Check whether the file exists.  If not, we return false to indicate that
+	 * there are no more files to restore.
+	 */
+	snprintf(source, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(source, &st) != 0)
+	{
+		int		elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m", source)));
+		return false;
+	}
+
+	copy_file(source, unconstify(char *, path));
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive", file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index bc1380e6f6..af4580dea9 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -7,7 +7,7 @@ basic_archive_sources = files(
 if host_system == 'windows'
   basic_archive_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
     '--NAME', 'basic_archive',
-    '--FILEDESC', 'basic_archive - basic archive module',])
+    '--FILEDESC', 'basic_archive - basic archive and recovery module',])
 endif
 
 basic_archive = shared_module('basic_archive',
@@ -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..ec8767d740
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,44 @@
+
+# Copyright (c) 2022, 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-modules.sgml b/doc/src/sgml/archive-modules.sgml
index ef02051f7f..53e657040b 100644
--- a/doc/src/sgml/archive-modules.sgml
+++ b/doc/src/sgml/archive-modules.sgml
@@ -1,34 +1,40 @@
 <!-- doc/src/sgml/archive-modules.sgml -->
 
 <chapter id="archive-modules">
- <title>Archive Modules</title>
+ <title>Archive and Recovery Modules</title>
  <indexterm zone="archive-modules">
-  <primary>Archive Modules</primary>
+  <primary>Archive and Recovery Modules</primary>
  </indexterm>
 
  <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 (e.g., <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>
   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"/>.
+  were successfully archived.  When a custom
+  <xref linkend="guc-restore-library"/> is configured, PostgreSQL will use the
+  module for recovery actions.  It is ultimately up to the module to decide how
+  to accomplish each task, but some recommendations are listed at
+  <xref linkend="backup-archiving-wal"/> and
+  <xref linkend="backup-pitr-recovery"/>.
  </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).
+  Archive and recovery modules must at least consist of an initialization
+  function (see <xref linkend="archive-module-init"/> and
+  <xref linkend="recovery-module-init"/>) and the required callbacks (see
+  <xref linkend="archive-module-callbacks"/> and
+  <xref linkend="recovery-module-callbacks"/>).  However, archive and recovery
+  modules are also permitted to do much more (e.g., declare GUCs and register
+  background workers).  A module may be used for both
+  <varname>archive_library</varname> and <varname>restore_library</varname>.
  </para>
 
  <para>
@@ -37,7 +43,7 @@
  </para>
 
  <sect1 id="archive-module-init">
-  <title>Initialization Functions</title>
+  <title>Archive Module Initialization Functions</title>
   <indexterm zone="archive-module-init">
    <primary>_PG_archive_module_init</primary>
   </indexterm>
@@ -64,6 +70,12 @@ typedef void (*ArchiveModuleInit) (struct ArchiveModuleCallbacks *cb);
    Only the <function>archive_file_cb</function> callback is required.  The
    others are optional.
   </para>
+
+  <note>
+   <para>
+    <varname>archive_library</varname> is only loaded in the archiver process.
+   </para>
+  </note>
  </sect1>
 
  <sect1 id="archive-module-callbacks">
@@ -129,6 +141,132 @@ typedef bool (*ArchiveFileCB) (const char *file, const char *path);
 
 <programlisting>
 typedef void (*ArchiveShutdownCB) (void);
+</programlisting>
+   </para>
+  </sect2>
+ </sect1>
+
+ <sect1 id="recovery-module-init">
+  <title>Recovery Module Initialization Functions</title>
+  <indexterm zone="recovery-module-init">
+   <primary>_PG_recovery_module_init</primary>
+  </indexterm>
+  <para>
+   A recovery library is loaded by dynamically loading a shared library with the
+   <xref linkend="guc-restore-library"/> as the library base name.  The normal
+   library search path is used to locate the library.  To provide the required
+   recovery module callbacks and to indicate that the library is actually a
+   recovery module, it needs to provide a function named
+   <function>_PG_recovery_module_init</function>.  This function is passed a
+   struct that needs to be filled with the callback function pointers for
+   individual actions.
+
+<programlisting>
+typedef struct RecoveryModuleCallbacks
+{
+    RecoveryRestoreCB restore_cb;
+    RecoveryArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndCB recovery_end_cb;
+    RecoveryShutdownCB shutdown_cb;
+} RecoveryModuleCallbacks;
+typedef void (*RecoveryModuleInit) (struct RecoveryModuleCallbacks *cb);
+</programlisting>
+
+   The <function>restore_cb</function> callback is required for archive
+   recovery, but it is optional for streaming replication.  The others are
+   always optional.
+  </para>
+
+  <note>
+   <para>
+    <varname>restore_library</varname> is only loaded in the startup and
+    checkpointer processes and in single-user mode.
+   </para>
+  </note>
+ </sect1>
+
+ <sect1 id="recovery-module-callbacks">
+  <title>Recovery Module Callbacks</title>
+  <para>
+   The recovery callbacks define the actual behavior of the module.  The server
+   will call them as required to execute recovery actions.
+  </para>
+
+  <sect2 id="recovery-module-restore">
+   <title>Restore Callback</title>
+   <para>
+    The <function>restore_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 (*RecoveryRestoreCB) (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>
+  </sect2>
+
+  <sect2 id="recovery-module-archive-cleanup">
+   <title>Archive Cleanup Callback</title>
+   <para>
+    The <function>archive_cleanup_cb</function> callback is called at every
+    restart point 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 (*RecoveryArchiveCleanupCB) (const char *lastRestartPointFileName);
+</programlisting>
+
+    <replaceable>lastRestartPointFileName</replaceable> will contain the name
+    of the file containing the last valid restart point, like in
+    <link linkend="recovery-module-restore"><function>restore_cb</function></link>.
+   </para>
+  </sect2>
+
+  <sect2 id="recovery-module-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
+    replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (const char *lastRestartPointFileName);
+</programlisting>
+
+    <replaceable>lastRestartPointFileName</replaceable> will contain the name
+    of the file containing the last valid restart point, like in
+    <link linkend="recovery-module-restore"><function>restore_cb</function></link>.
+   </para>
+  </sect2>
+
+  <sect2 id="recovery-module-shutdown">
+   <title>Shutdown Callback</title>
+   <para>
+    The <function>shutdown_cb</function> callback is called when a process that
+    has loaded the recovery 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.
+
+<programlisting>
+typedef void (*RecoveryShutdownCB) (void);
 </programlisting>
    </para>
   </sect2>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index 8bab521718..d6d34078fc 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1181,9 +1181,27 @@ 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>,
-    which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>
+    that defines a restore callback, which tells
+    <productname>PostgreSQL</productname> how to retrieve archived 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,
+    recovery modules can be more performant 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="archive-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.
@@ -1202,14 +1220,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 function returns
+    <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 function provided by the
+    <varname>restore_library</varname> emits an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1233,7 +1257,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 60f23d2855..11fd670dbc 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 recovery
+  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 recovery modules.  For
+  more information about archive and recovery modules, see
+  see <xref linkend="archive-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 recovery 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,7 +65,8 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
-   Server crashes may leave temporary files with the prefix
+   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
    remove such files while the server is running as long as they are unrelated
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 77574e2d4e..039d3360ac 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3773,7 +3773,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
@@ -3801,7 +3802,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,
@@ -3836,7 +3838,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 recovery actions, including retrieving archived
+        segments of the WAL file series 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 recovery.
+        For more information, see <xref linkend="archive-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>
@@ -3881,7 +3918,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>
@@ -3910,11 +3950,13 @@ 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>
-
     </variablelist>
 
   </sect2>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index f180607528..6266e2df7f 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,9 +639,11 @@ 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
-    reaches the end of WAL available there and <varname>restore_command</varname>
-    fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s restore callback.
+    Once it reaches the end of WAL available there and
+    <varname>restore_command</varname> or the restore callback 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
     from the last valid record found in archive or <filename>pg_wal</filename>. If that fails
@@ -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 restore callbacks provided by
+     <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,10 @@ 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
+    <function>archive_cleanup_cb</function> callback function 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/shell_restore.c b/src/backend/access/transam/shell_restore.c
index 68bf6e8556..eb101b51fb 100644
--- a/src/backend/access/transam/shell_restore.c
+++ b/src/backend/access/transam/shell_restore.c
@@ -3,7 +3,8 @@
  * shell_restore.c
  *
  * These recovery 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 recovery logic.
  *
  * Copyright (c) 2022, PostgreSQL Global Development Group
  *
@@ -23,11 +24,25 @@
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static bool shell_restore(const char *file, const char *path,
+						  const char *lastRestartPointFileName);
+static void shell_archive_cleanup(const char *lastRestartPointFileName);
+static void shell_recovery_end(const char *lastRestartPointFileName);
 static bool ExecuteRecoveryCommand(const char *command,
 								   const char *commandName, bool failOnSignal,
 								   bool exitOnSigterm, uint32 wait_event_info,
 								   int fail_elevel);
 
+void
+shell_restore_init(RecoveryModuleCallbacks *cb)
+{
+	AssertVariableIsOfType(&shell_restore_init, RecoveryModuleInit);
+
+	cb->restore_cb = shell_restore;
+	cb->archive_cleanup_cb = shell_archive_cleanup;
+	cb->recovery_end_cb = shell_recovery_end;
+}
+
 bool
 shell_restore(const char *file, const char *path,
 			  const char *lastRestartPointFileName)
@@ -70,7 +85,7 @@ shell_restore(const char *file, const char *path,
 	return ret;
 }
 
-void
+static void
 shell_archive_cleanup(const char *lastRestartPointFileName)
 {
 	char	   *cmd;
@@ -83,7 +98,7 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 	pfree(cmd);
 }
 
-void
+static void
 shell_recovery_end(const char *lastRestartPointFileName)
 {
 	char	   *cmd;
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index fdce12614a..c4ad571327 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4883,10 +4883,11 @@ static void
 CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
+
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 */
-	if (recoveryEndCommand && strcmp(recoveryEndCommand, "") != 0)
+	if (RecoveryContext.recovery_end_cb)
 	{
 		char		lastRestartPointFname[MAXPGPATH];
 		XLogSegNo	restartSegNo;
@@ -4903,7 +4904,7 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 		XLogFileName(lastRestartPointFname, restartTli, restartSegNo,
 					 wal_segment_size);
 
-		shell_recovery_end(lastRestartPointFname);
+		RecoveryContext.recovery_end_cb(lastRestartPointFname);
 	}
 
 	/*
@@ -7318,9 +7319,9 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Execute the archive-cleanup callback, if any.
 	 */
-	if (archiveCleanupCommand && strcmp(archiveCleanupCommand, "") != 0)
+	if (RecoveryContext.archive_cleanup_cb)
 	{
 		char		lastRestartPointFname[MAXPGPATH];
 		XLogSegNo	restartSegNo;
@@ -7337,7 +7338,7 @@ CreateRestartPoint(int flags)
 		XLogFileName(lastRestartPointFname, restartTli, restartSegNo,
 					 wal_segment_size);
 
-		shell_archive_cleanup(lastRestartPointFname);
+		RecoveryContext.archive_cleanup_cb(lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index b5cb060d55..fdab7dad43 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -22,7 +22,9 @@
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
+#include "access/xlogrecovery.h"
 #include "common/archive.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/startup.h"
@@ -32,6 +34,11 @@
 #include "storage/ipc.h"
 #include "storage/lwlock.h"
 
+/*
+ * Global context for recovery-related callbacks.
+ */
+RecoveryModuleCallbacks RecoveryContext;
+
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
  * If successful, fill "path" with its complete path (note that this will be
@@ -71,7 +78,7 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 		goto not_available;
 
 	/* In standby mode, restore_command might not be supplied */
-	if (recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0)
+	if (RecoveryContext.restore_cb == NULL)
 		goto not_available;
 
 	/*
@@ -149,14 +156,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 		XLogFileName(lastRestartPointFname, 0, 0L, wal_segment_size);
 
 	/*
-	 * Check signals before restore command and reset afterwards.
+	 * Check signals before restore callback and reset afterwards.
 	 */
 	PreRestoreCommand();
 
 	/*
 	 * Copy xlog from archival storage to XLOGDIR
 	 */
-	ret = shell_restore(xlogfname, xlogpath, lastRestartPointFname);
+	ret = RecoveryContext.restore_cb(xlogfname, xlogpath,
+									 lastRestartPointFname);
 
 	PostRestoreCommand();
 
@@ -603,3 +611,59 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the recovery callbacks into our global RecoveryContext.  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
+LoadRecoveryCallbacks(void)
+{
+	RecoveryModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_recovery_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RecoveryModuleInit)
+			load_external_function(restoreLibrary, "_PG_recovery_module_init",
+								   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("recovery modules have to define the symbol "
+						"_PG_recovery_module_init")));
+
+	memset(&RecoveryContext, 0, sizeof(RecoveryModuleCallbacks));
+	(*init) (&RecoveryContext);
+
+	/*
+	 * If using shell commands, remove callbacks for any commands that are not
+	 * set.
+	 */
+	if (restoreLibrary[0] == '\0')
+	{
+		if (recoveryRestoreCommand[0] == '\0')
+			RecoveryContext.restore_cb = NULL;
+		if (archiveCleanupCommand[0] == '\0')
+			RecoveryContext.archive_cleanup_cb = NULL;
+		if (recoveryEndCommand[0] == '\0')
+			RecoveryContext.recovery_end_cb = NULL;
+	}
+}
+
+/*
+ * Call the shutdown callback of the loaded recovery module, if defined.
+ */
+void
+call_recovery_module_shutdown_cb(int code, Datum arg)
+{
+	if (RecoveryContext.shutdown_cb)
+		RecoveryContext.shutdown_cb();
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 5e65785306..db0cd4469a 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -80,6 +80,7 @@ const struct config_enum_entry recovery_target_action_options[] = {
 
 /* options formerly taken from recovery.conf for archive recovery */
 char	   *recoveryRestoreCommand = NULL;
+char	   *restoreLibrary = NULL;
 char	   *recoveryEndCommand = NULL;
 char	   *archiveCleanupCommand = NULL;
 RecoveryTargetType recoveryTarget = RECOVERY_TARGET_UNSET;
@@ -1053,24 +1054,37 @@ validateRecoveryParameters(void)
 	if (!ArchiveRecoveryRequested)
 		return;
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryRestoreCommand, "restore_command");
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryEndCommand, "recovery_end_command");
+	before_shmem_exit(call_recovery_module_shutdown_cb, 0);
+	LoadRecoveryCallbacks();
+
 	/*
 	 * Check for compulsory parameters
 	 */
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			RecoveryContext.restore_cb == NULL)
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
-					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
+					(errmsg("specified neither primary_conninfo nor restore_command "
+							"nor a restore_library that defines a restore callback"),
+					 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 (RecoveryContext.restore_cb == NULL)
 			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 restore_library that defines "
+							"a restore callback when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index de0bbbfa79..6350fd0b83 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -38,6 +38,7 @@
 
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
+#include "access/xlogarchive.h"
 #include "access/xlogrecovery.h"
 #include "libpq/pqsignal.h"
 #include "miscadmin.h"
@@ -222,6 +223,16 @@ CheckpointerMain(void)
 	 */
 	before_shmem_exit(pgstat_before_server_shutdown, 0);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.  We do this before setting up the exception handler
+	 * so that any problems result in a server crash shortly after startup.
+	 */
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   archiveCleanupCommand, "archive_cleanup_command");
+	before_shmem_exit(call_recovery_module_shutdown_cb, 0);
+	LoadRecoveryCallbacks();
+
 	/*
 	 * Create a memory context that we will do all our work in.  We do this so
 	 * that we can reset the context during error recovery and thereby avoid
@@ -548,6 +559,9 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -563,6 +577,18 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+								   archiveCleanupCommand, "archive_cleanup_command");
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			call_recovery_module_shutdown_cb(0, (Datum) 0);
+			LoadRecoveryCallbacks();
+		}
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c
index 8ecdb9ca23..8e91f2d70f 100644
--- a/src/backend/postmaster/pgarch.c
+++ b/src/backend/postmaster/pgarch.c
@@ -831,11 +831,8 @@ 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.")));
+	CheckMutuallyExclusiveGUCs(XLogArchiveLibrary, "archive_library",
+							   XLogArchiveCommand, "archive_command");
 
 	memset(&ArchiveContext, 0, sizeof(ArchiveModuleCallbacks));
 
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index 8786186898..f9ff2b5583 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -20,6 +20,7 @@
 #include "postgres.h"
 
 #include "access/xlog.h"
+#include "access/xlogarchive.h"
 #include "access/xlogrecovery.h"
 #include "access/xlogutils.h"
 #include "libpq/pqsignal.h"
@@ -133,13 +134,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 recovery 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;
@@ -161,6 +166,22 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryRestoreCommand, "restore_command");
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryEndCommand, "recovery_end_command");
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_recovery_module_shutdown_cb(0, (Datum) 0);
+		LoadRecoveryCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index d52069f446..7858e9a649 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -6880,3 +6880,17 @@ call_enum_check_hook(struct config_enum *conf, int *newval, void **extra,
 
 	return true;
 }
+
+/*
+ * ERROR if both parameters are set.
+ */
+void
+CheckMutuallyExclusiveGUCs(const char *p1val, const char *p1name,
+						   const char *p2val, const char *p2name)
+{
+	if (p1val[0] != '\0' && p2val[0] != '\0')
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("both %s and %s set", p1name, p2name),
+				 errdetail("Only one of %s, %s may be set.", p1name, p2name)));
+}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 5025e80f89..a8a516b0c6 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -3776,6 +3776,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 recovery 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 4cceda4162..38fb3e0823 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -269,6 +269,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 recovery 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/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 59fc7bc105..756f0898b5 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -400,5 +400,6 @@ extern PGDLLIMPORT bool ArchiveRecoveryRequested;
 extern PGDLLIMPORT bool InArchiveRecovery;
 extern PGDLLIMPORT bool StandbyMode;
 extern PGDLLIMPORT char *recoveryRestoreCommand;
+extern PGDLLIMPORT char *restoreLibrary;
 
 #endif							/* XLOG_INTERNAL_H */
diff --git a/src/include/access/xlogarchive.h b/src/include/access/xlogarchive.h
index 299304703e..71c9b88165 100644
--- a/src/include/access/xlogarchive.h
+++ b/src/include/access/xlogarchive.h
@@ -30,9 +30,45 @@ extern bool XLogArchiveIsReady(const char *xlog);
 extern bool XLogArchiveIsReadyOrDone(const char *xlog);
 extern void XLogArchiveCleanup(const char *xlog);
 
-extern bool shell_restore(const char *file, const char *path,
-						  const char *lastRestartPointFileName);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Recovery module callbacks
+ *
+ * These callback functions should be defined by recovery libraries and
+ * returned via _PG_recovery_module_init().  For more information about the
+ * purpose of each callback, refer to the recovery modules documentation.
+ */
+typedef bool (*RecoveryRestoreCB) (const char *file, const char *path,
+								   const char *lastRestartPointFileName);
+typedef void (*RecoveryArchiveCleanupCB) (const char *lastRestartPointFileName);
+typedef void (*RecoveryEndCB) (const char *lastRestartPointFileName);
+typedef void (*RecoveryShutdownCB) (void);
+
+typedef struct RecoveryModuleCallbacks
+{
+	RecoveryRestoreCB restore_cb;
+	RecoveryArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndCB recovery_end_cb;
+	RecoveryShutdownCB shutdown_cb;
+} RecoveryModuleCallbacks;
+
+extern RecoveryModuleCallbacks RecoveryContext;
+
+/*
+ * Type of the shared library symbol _PG_recovery_module_init that is looked up
+ * when loading a recovery library.
+ */
+typedef void (*RecoveryModuleInit) (RecoveryModuleCallbacks *cb);
+
+extern PGDLLEXPORT void _PG_recovery_module_init(RecoveryModuleCallbacks *cb);
+
+extern void LoadRecoveryCallbacks(void);
+extern void call_recovery_module_shutdown_cb(int code, Datum arg);
+
+/*
+ * Since the logic for recovery via a shell command is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern void shell_restore_init(RecoveryModuleCallbacks *cb);
 
 #endif							/* XLOG_ARCHIVE_H */
diff --git a/src/include/access/xlogrecovery.h b/src/include/access/xlogrecovery.h
index 47c29350f5..35d1d09374 100644
--- a/src/include/access/xlogrecovery.h
+++ b/src/include/access/xlogrecovery.h
@@ -55,6 +55,7 @@ extern PGDLLIMPORT int recovery_min_apply_delay;
 extern PGDLLIMPORT char *PrimaryConnInfo;
 extern PGDLLIMPORT char *PrimarySlotName;
 extern PGDLLIMPORT char *recoveryRestoreCommand;
+extern PGDLLIMPORT char *restoreLibrary;
 extern PGDLLIMPORT char *recoveryEndCommand;
 extern PGDLLIMPORT char *archiveCleanupCommand;
 
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index ba89d013e6..947597247f 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -404,6 +404,8 @@ extern void *guc_malloc(int elevel, size_t size);
 extern pg_nodiscard void *guc_realloc(int elevel, void *old, size_t size);
 extern char *guc_strdup(int elevel, const char *src);
 extern void guc_free(void *ptr);
+extern void CheckMutuallyExclusiveGUCs(const char *p1val, const char *p1name,
+									   const char *p2val, const char *p2name);
 
 #ifdef EXEC_BACKEND
 extern void write_nondefault_variables(GucContext context);
-- 
2.25.1


--C7zPtVaVf+AK4Oqc--





^ permalink  raw  reply  [nested|flat] 17+ messages in thread

* [PATCH v8 2/2] Allow recovery via loadable modules.
@ 2022-12-10 03:40 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 17+ messages in thread

From: Nathan Bossart @ 2022-12-10 03:40 UTC (permalink / raw)

This adds the restore_library parameter to allow archive recovery
via a loadable module, rather than running shell commands.
---
 contrib/basic_archive/Makefile                |   4 +-
 contrib/basic_archive/basic_archive.c         |  67 ++++++-
 contrib/basic_archive/meson.build             |   7 +-
 contrib/basic_archive/t/001_restore.pl        |  44 +++++
 doc/src/sgml/archive-modules.sgml             | 168 ++++++++++++++++--
 doc/src/sgml/backup.sgml                      |  43 ++++-
 doc/src/sgml/basic-archive.sgml               |  33 ++--
 doc/src/sgml/config.sgml                      |  54 +++++-
 doc/src/sgml/high-availability.sgml           |  23 ++-
 src/backend/access/transam/shell_restore.c    |  21 ++-
 src/backend/access/transam/xlog.c             |  13 +-
 src/backend/access/transam/xlogarchive.c      |  70 +++++++-
 src/backend/access/transam/xlogrecovery.c     |  26 ++-
 src/backend/postmaster/checkpointer.c         |  26 +++
 src/backend/postmaster/pgarch.c               |   7 +-
 src/backend/postmaster/startup.c              |  23 ++-
 src/backend/utils/misc/guc.c                  |  14 ++
 src/backend/utils/misc/guc_tables.c           |  10 ++
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/access/xlog_internal.h            |   1 +
 src/include/access/xlogarchive.h              |  44 ++++-
 src/include/access/xlogrecovery.h             |   1 +
 src/include/utils/guc.h                       |   2 +
 23 files changed, 618 insertions(+), 84 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 55d299d650..487dc563f3 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -1,7 +1,7 @@
 # contrib/basic_archive/Makefile
 
 MODULES = basic_archive
-PGFILEDESC = "basic_archive - basic archive module"
+PGFILEDESC = "basic_archive - basic archive and recovery module"
 
 REGRESS = basic_archive
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.conf
@@ -9,6 +9,8 @@ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.c
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
 
+TAP_TESTS = 1
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index 28cbb6cce0..8c333c8f99 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
@@ -30,6 +35,7 @@
 #include <sys/time.h>
 #include <unistd.h>
 
+#include "access/xlogarchive.h"
 #include "common/int.h"
 #include "miscadmin.h"
 #include "postmaster/pgarch.h"
@@ -48,6 +54,8 @@ static bool basic_archive_file(const char *file, const char *path);
 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 bool basic_restore_file(const char *file, const char *path,
+							   const char *lastRestartPointFileName);
 
 /*
  * _PG_init
@@ -87,6 +95,19 @@ _PG_archive_module_init(ArchiveModuleCallbacks *cb)
 	cb->archive_file_cb = basic_archive_file;
 }
 
+/*
+ * _PG_recovery_module_init
+ *
+ * Returns the module's restore callback.
+ */
+void
+_PG_recovery_module_init(RecoveryModuleCallbacks *cb)
+{
+	AssertVariableIsOfType(&_PG_recovery_module_init, RecoveryModuleInit);
+
+	cb->restore_cb = basic_restore_file;
+}
+
 /*
  * check_archive_directory
  *
@@ -99,8 +120,8 @@ check_archive_directory(char **newval, void **extra, GucSource source)
 
 	/*
 	 * The default value is an empty string, so we have to accept that value.
-	 * Our check_configured callback also checks for this and prevents
-	 * archiving from proceeding if it is still empty.
+	 * Our check_configured and restore callbacks also check for this and
+	 * prevent archiving or recovery from proceeding if it is still empty.
 	 */
 	if (*newval == NULL || *newval[0] == '\0')
 		return true;
@@ -368,3 +389,45 @@ compare_files(const char *file1, const char *file2)
 
 	return ret;
 }
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file from the WAL archives.
+ */
+static bool
+basic_restore_file(const char *file, const char *path,
+				   const char *lastRestartPointFileName)
+{
+	char		source[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive", file)));
+
+	if (archive_directory == NULL || archive_directory[0] == '\0')
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"basic_archive.archive_directory\" is not set")));
+
+	/*
+	 * Check whether the file exists.  If not, we return false to indicate that
+	 * there are no more files to restore.
+	 */
+	snprintf(source, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(source, &st) != 0)
+	{
+		int		elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m", source)));
+		return false;
+	}
+
+	copy_file(source, unconstify(char *, path));
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive", file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index bc1380e6f6..af4580dea9 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -7,7 +7,7 @@ basic_archive_sources = files(
 if host_system == 'windows'
   basic_archive_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
     '--NAME', 'basic_archive',
-    '--FILEDESC', 'basic_archive - basic archive module',])
+    '--FILEDESC', 'basic_archive - basic archive and recovery module',])
 endif
 
 basic_archive = shared_module('basic_archive',
@@ -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..ec8767d740
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,44 @@
+
+# Copyright (c) 2022, 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-modules.sgml b/doc/src/sgml/archive-modules.sgml
index ef02051f7f..53e657040b 100644
--- a/doc/src/sgml/archive-modules.sgml
+++ b/doc/src/sgml/archive-modules.sgml
@@ -1,34 +1,40 @@
 <!-- doc/src/sgml/archive-modules.sgml -->
 
 <chapter id="archive-modules">
- <title>Archive Modules</title>
+ <title>Archive and Recovery Modules</title>
  <indexterm zone="archive-modules">
-  <primary>Archive Modules</primary>
+  <primary>Archive and Recovery Modules</primary>
  </indexterm>
 
  <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 (e.g., <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>
   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"/>.
+  were successfully archived.  When a custom
+  <xref linkend="guc-restore-library"/> is configured, PostgreSQL will use the
+  module for recovery actions.  It is ultimately up to the module to decide how
+  to accomplish each task, but some recommendations are listed at
+  <xref linkend="backup-archiving-wal"/> and
+  <xref linkend="backup-pitr-recovery"/>.
  </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).
+  Archive and recovery modules must at least consist of an initialization
+  function (see <xref linkend="archive-module-init"/> and
+  <xref linkend="recovery-module-init"/>) and the required callbacks (see
+  <xref linkend="archive-module-callbacks"/> and
+  <xref linkend="recovery-module-callbacks"/>).  However, archive and recovery
+  modules are also permitted to do much more (e.g., declare GUCs and register
+  background workers).  A module may be used for both
+  <varname>archive_library</varname> and <varname>restore_library</varname>.
  </para>
 
  <para>
@@ -37,7 +43,7 @@
  </para>
 
  <sect1 id="archive-module-init">
-  <title>Initialization Functions</title>
+  <title>Archive Module Initialization Functions</title>
   <indexterm zone="archive-module-init">
    <primary>_PG_archive_module_init</primary>
   </indexterm>
@@ -64,6 +70,12 @@ typedef void (*ArchiveModuleInit) (struct ArchiveModuleCallbacks *cb);
    Only the <function>archive_file_cb</function> callback is required.  The
    others are optional.
   </para>
+
+  <note>
+   <para>
+    <varname>archive_library</varname> is only loaded in the archiver process.
+   </para>
+  </note>
  </sect1>
 
  <sect1 id="archive-module-callbacks">
@@ -129,6 +141,132 @@ typedef bool (*ArchiveFileCB) (const char *file, const char *path);
 
 <programlisting>
 typedef void (*ArchiveShutdownCB) (void);
+</programlisting>
+   </para>
+  </sect2>
+ </sect1>
+
+ <sect1 id="recovery-module-init">
+  <title>Recovery Module Initialization Functions</title>
+  <indexterm zone="recovery-module-init">
+   <primary>_PG_recovery_module_init</primary>
+  </indexterm>
+  <para>
+   A recovery library is loaded by dynamically loading a shared library with the
+   <xref linkend="guc-restore-library"/> as the library base name.  The normal
+   library search path is used to locate the library.  To provide the required
+   recovery module callbacks and to indicate that the library is actually a
+   recovery module, it needs to provide a function named
+   <function>_PG_recovery_module_init</function>.  This function is passed a
+   struct that needs to be filled with the callback function pointers for
+   individual actions.
+
+<programlisting>
+typedef struct RecoveryModuleCallbacks
+{
+    RecoveryRestoreCB restore_cb;
+    RecoveryArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndCB recovery_end_cb;
+    RecoveryShutdownCB shutdown_cb;
+} RecoveryModuleCallbacks;
+typedef void (*RecoveryModuleInit) (struct RecoveryModuleCallbacks *cb);
+</programlisting>
+
+   The <function>restore_cb</function> callback is required for archive
+   recovery, but it is optional for streaming replication.  The others are
+   always optional.
+  </para>
+
+  <note>
+   <para>
+    <varname>restore_library</varname> is only loaded in the startup and
+    checkpointer processes and in single-user mode.
+   </para>
+  </note>
+ </sect1>
+
+ <sect1 id="recovery-module-callbacks">
+  <title>Recovery Module Callbacks</title>
+  <para>
+   The recovery callbacks define the actual behavior of the module.  The server
+   will call them as required to execute recovery actions.
+  </para>
+
+  <sect2 id="recovery-module-restore">
+   <title>Restore Callback</title>
+   <para>
+    The <function>restore_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 (*RecoveryRestoreCB) (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>
+  </sect2>
+
+  <sect2 id="recovery-module-archive-cleanup">
+   <title>Archive Cleanup Callback</title>
+   <para>
+    The <function>archive_cleanup_cb</function> callback is called at every
+    restart point 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 (*RecoveryArchiveCleanupCB) (const char *lastRestartPointFileName);
+</programlisting>
+
+    <replaceable>lastRestartPointFileName</replaceable> will contain the name
+    of the file containing the last valid restart point, like in
+    <link linkend="recovery-module-restore"><function>restore_cb</function></link>.
+   </para>
+  </sect2>
+
+  <sect2 id="recovery-module-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
+    replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (const char *lastRestartPointFileName);
+</programlisting>
+
+    <replaceable>lastRestartPointFileName</replaceable> will contain the name
+    of the file containing the last valid restart point, like in
+    <link linkend="recovery-module-restore"><function>restore_cb</function></link>.
+   </para>
+  </sect2>
+
+  <sect2 id="recovery-module-shutdown">
+   <title>Shutdown Callback</title>
+   <para>
+    The <function>shutdown_cb</function> callback is called when a process that
+    has loaded the recovery 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.
+
+<programlisting>
+typedef void (*RecoveryShutdownCB) (void);
 </programlisting>
    </para>
   </sect2>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index be05a33205..f44135061d 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1180,9 +1180,27 @@ 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>,
-    which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>
+    that defines a restore callback, which tells
+    <productname>PostgreSQL</productname> how to retrieve archived 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,
+    recovery modules can be more performant 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="archive-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 +1219,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 function returns
+    <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 function provided by the
+    <varname>restore_library</varname> emits an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1232,7 +1256,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 60f23d2855..11fd670dbc 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 recovery
+  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 recovery modules.  For
+  more information about archive and recovery modules, see
+  see <xref linkend="archive-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 recovery 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,7 +65,8 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
-   Server crashes may leave temporary files with the prefix
+   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
    remove such files while the server is running as long as they are unrelated
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 77574e2d4e..039d3360ac 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3773,7 +3773,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
@@ -3801,7 +3802,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,
@@ -3836,7 +3838,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 recovery actions, including retrieving archived
+        segments of the WAL file series 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 recovery.
+        For more information, see <xref linkend="archive-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>
@@ -3881,7 +3918,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>
@@ -3910,11 +3950,13 @@ 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>
-
     </variablelist>
 
   </sect2>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index f180607528..6266e2df7f 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,9 +639,11 @@ 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
-    reaches the end of WAL available there and <varname>restore_command</varname>
-    fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s restore callback.
+    Once it reaches the end of WAL available there and
+    <varname>restore_command</varname> or the restore callback 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
     from the last valid record found in archive or <filename>pg_wal</filename>. If that fails
@@ -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 restore callbacks provided by
+     <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,10 @@ 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
+    <function>archive_cleanup_cb</function> callback function 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/shell_restore.c b/src/backend/access/transam/shell_restore.c
index 4752be0b9f..90283c601b 100644
--- a/src/backend/access/transam/shell_restore.c
+++ b/src/backend/access/transam/shell_restore.c
@@ -4,7 +4,8 @@
  *		Recovery functions for a user-specified shell command.
  *
  * These recovery functions use a user-specified shell command (e.g. based
- * on the GUC restore_command).
+ * on the GUC restore_command).  It is used as the default, but other
+ * modules may define their own recovery logic.
  *
  * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
@@ -24,11 +25,25 @@
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static bool shell_restore(const char *file, const char *path,
+						  const char *lastRestartPointFileName);
+static void shell_archive_cleanup(const char *lastRestartPointFileName);
+static void shell_recovery_end(const char *lastRestartPointFileName);
 static bool ExecuteRecoveryCommand(const char *command,
 								   const char *commandName,
 								   bool failOnSignal, bool exitOnSigterm,
 								   uint32 wait_event_info, int fail_elevel);
 
+void
+shell_restore_init(RecoveryModuleCallbacks *cb)
+{
+	AssertVariableIsOfType(&shell_restore_init, RecoveryModuleInit);
+
+	cb->restore_cb = shell_restore;
+	cb->archive_cleanup_cb = shell_archive_cleanup;
+	cb->recovery_end_cb = shell_recovery_end;
+}
+
 /*
  * Attempt to execute a shell-based restore command.
  *
@@ -83,7 +98,7 @@ shell_restore(const char *file, const char *path,
 /*
  * Attempt to execute a shell-based archive cleanup command.
  */
-void
+static void
 shell_archive_cleanup(const char *lastRestartPointFileName)
 {
 	char	   *cmd;
@@ -99,7 +114,7 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 /*
  * Attempt to execute a shell-based end-of-recovery command.
  */
-void
+static void
 shell_recovery_end(const char *lastRestartPointFileName)
 {
 	char	   *cmd;
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 8f47fb7570..ae537cd87f 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4884,15 +4884,16 @@ static void
 CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
+
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 */
-	if (recoveryEndCommand && strcmp(recoveryEndCommand, "") != 0)
+	if (RecoveryContext.recovery_end_cb)
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_recovery_end(lastRestartPointFname);
+		RecoveryContext.recovery_end_cb(lastRestartPointFname);
 	}
 
 	/*
@@ -7307,14 +7308,14 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Execute the archive-cleanup callback, if any.
 	 */
-	if (archiveCleanupCommand && strcmp(archiveCleanupCommand, "") != 0)
+	if (RecoveryContext.archive_cleanup_cb)
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_archive_cleanup(lastRestartPointFname);
+		RecoveryContext.archive_cleanup_cb(lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index 4b89addf97..4af5689c25 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -22,6 +22,8 @@
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
+#include "access/xlogrecovery.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/startup.h"
@@ -31,6 +33,11 @@
 #include "storage/ipc.h"
 #include "storage/lwlock.h"
 
+/*
+ * Global context for recovery-related callbacks.
+ */
+RecoveryModuleCallbacks RecoveryContext;
+
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
  * If successful, fill "path" with its complete path (note that this will be
@@ -70,7 +77,7 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 		goto not_available;
 
 	/* In standby mode, restore_command might not be supplied */
-	if (recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0)
+	if (RecoveryContext.restore_cb == NULL)
 		goto not_available;
 
 	/*
@@ -148,14 +155,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 		XLogFileName(lastRestartPointFname, 0, 0L, wal_segment_size);
 
 	/*
-	 * Check signals before restore command and reset afterwards.
+	 * Check signals before restore callback and reset afterwards.
 	 */
 	PreRestoreCommand();
 
 	/*
 	 * Copy xlog from archival storage to XLOGDIR
 	 */
-	ret = shell_restore(xlogfname, xlogpath, lastRestartPointFname);
+	ret = RecoveryContext.restore_cb(xlogfname, xlogpath,
+									 lastRestartPointFname);
 
 	PostRestoreCommand();
 
@@ -602,3 +610,59 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the recovery callbacks into our global RecoveryContext.  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
+LoadRecoveryCallbacks(void)
+{
+	RecoveryModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_recovery_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RecoveryModuleInit)
+			load_external_function(restoreLibrary, "_PG_recovery_module_init",
+								   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("recovery modules have to define the symbol "
+						"_PG_recovery_module_init")));
+
+	memset(&RecoveryContext, 0, sizeof(RecoveryModuleCallbacks));
+	(*init) (&RecoveryContext);
+
+	/*
+	 * If using shell commands, remove callbacks for any commands that are not
+	 * set.
+	 */
+	if (restoreLibrary[0] == '\0')
+	{
+		if (recoveryRestoreCommand[0] == '\0')
+			RecoveryContext.restore_cb = NULL;
+		if (archiveCleanupCommand[0] == '\0')
+			RecoveryContext.archive_cleanup_cb = NULL;
+		if (recoveryEndCommand[0] == '\0')
+			RecoveryContext.recovery_end_cb = NULL;
+	}
+}
+
+/*
+ * Call the shutdown callback of the loaded recovery module, if defined.
+ */
+void
+call_recovery_module_shutdown_cb(int code, Datum arg)
+{
+	if (RecoveryContext.shutdown_cb)
+		RecoveryContext.shutdown_cb();
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 5e65785306..db0cd4469a 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -80,6 +80,7 @@ const struct config_enum_entry recovery_target_action_options[] = {
 
 /* options formerly taken from recovery.conf for archive recovery */
 char	   *recoveryRestoreCommand = NULL;
+char	   *restoreLibrary = NULL;
 char	   *recoveryEndCommand = NULL;
 char	   *archiveCleanupCommand = NULL;
 RecoveryTargetType recoveryTarget = RECOVERY_TARGET_UNSET;
@@ -1053,24 +1054,37 @@ validateRecoveryParameters(void)
 	if (!ArchiveRecoveryRequested)
 		return;
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryRestoreCommand, "restore_command");
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryEndCommand, "recovery_end_command");
+	before_shmem_exit(call_recovery_module_shutdown_cb, 0);
+	LoadRecoveryCallbacks();
+
 	/*
 	 * Check for compulsory parameters
 	 */
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			RecoveryContext.restore_cb == NULL)
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
-					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
+					(errmsg("specified neither primary_conninfo nor restore_command "
+							"nor a restore_library that defines a restore callback"),
+					 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 (RecoveryContext.restore_cb == NULL)
 			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 restore_library that defines "
+							"a restore callback when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index de0bbbfa79..6350fd0b83 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -38,6 +38,7 @@
 
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
+#include "access/xlogarchive.h"
 #include "access/xlogrecovery.h"
 #include "libpq/pqsignal.h"
 #include "miscadmin.h"
@@ -222,6 +223,16 @@ CheckpointerMain(void)
 	 */
 	before_shmem_exit(pgstat_before_server_shutdown, 0);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.  We do this before setting up the exception handler
+	 * so that any problems result in a server crash shortly after startup.
+	 */
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   archiveCleanupCommand, "archive_cleanup_command");
+	before_shmem_exit(call_recovery_module_shutdown_cb, 0);
+	LoadRecoveryCallbacks();
+
 	/*
 	 * Create a memory context that we will do all our work in.  We do this so
 	 * that we can reset the context during error recovery and thereby avoid
@@ -548,6 +559,9 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -563,6 +577,18 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+								   archiveCleanupCommand, "archive_cleanup_command");
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			call_recovery_module_shutdown_cb(0, (Datum) 0);
+			LoadRecoveryCallbacks();
+		}
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c
index 8ecdb9ca23..8e91f2d70f 100644
--- a/src/backend/postmaster/pgarch.c
+++ b/src/backend/postmaster/pgarch.c
@@ -831,11 +831,8 @@ 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.")));
+	CheckMutuallyExclusiveGUCs(XLogArchiveLibrary, "archive_library",
+							   XLogArchiveCommand, "archive_command");
 
 	memset(&ArchiveContext, 0, sizeof(ArchiveModuleCallbacks));
 
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index 8786186898..f9ff2b5583 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -20,6 +20,7 @@
 #include "postgres.h"
 
 #include "access/xlog.h"
+#include "access/xlogarchive.h"
 #include "access/xlogrecovery.h"
 #include "access/xlogutils.h"
 #include "libpq/pqsignal.h"
@@ -133,13 +134,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 recovery 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;
@@ -161,6 +166,22 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryRestoreCommand, "restore_command");
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryEndCommand, "recovery_end_command");
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_recovery_module_shutdown_cb(0, (Datum) 0);
+		LoadRecoveryCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index d52069f446..7858e9a649 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -6880,3 +6880,17 @@ call_enum_check_hook(struct config_enum *conf, int *newval, void **extra,
 
 	return true;
 }
+
+/*
+ * ERROR if both parameters are set.
+ */
+void
+CheckMutuallyExclusiveGUCs(const char *p1val, const char *p1name,
+						   const char *p2val, const char *p2name)
+{
+	if (p1val[0] != '\0' && p2val[0] != '\0')
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("both %s and %s set", p1name, p2name),
+				 errdetail("Only one of %s, %s may be set.", p1name, p2name)));
+}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 5025e80f89..a8a516b0c6 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -3776,6 +3776,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 recovery 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 4cceda4162..38fb3e0823 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -269,6 +269,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 recovery 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/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 59fc7bc105..756f0898b5 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -400,5 +400,6 @@ extern PGDLLIMPORT bool ArchiveRecoveryRequested;
 extern PGDLLIMPORT bool InArchiveRecovery;
 extern PGDLLIMPORT bool StandbyMode;
 extern PGDLLIMPORT char *recoveryRestoreCommand;
+extern PGDLLIMPORT char *restoreLibrary;
 
 #endif							/* XLOG_INTERNAL_H */
diff --git a/src/include/access/xlogarchive.h b/src/include/access/xlogarchive.h
index 299304703e..71c9b88165 100644
--- a/src/include/access/xlogarchive.h
+++ b/src/include/access/xlogarchive.h
@@ -30,9 +30,45 @@ extern bool XLogArchiveIsReady(const char *xlog);
 extern bool XLogArchiveIsReadyOrDone(const char *xlog);
 extern void XLogArchiveCleanup(const char *xlog);
 
-extern bool shell_restore(const char *file, const char *path,
-						  const char *lastRestartPointFileName);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Recovery module callbacks
+ *
+ * These callback functions should be defined by recovery libraries and
+ * returned via _PG_recovery_module_init().  For more information about the
+ * purpose of each callback, refer to the recovery modules documentation.
+ */
+typedef bool (*RecoveryRestoreCB) (const char *file, const char *path,
+								   const char *lastRestartPointFileName);
+typedef void (*RecoveryArchiveCleanupCB) (const char *lastRestartPointFileName);
+typedef void (*RecoveryEndCB) (const char *lastRestartPointFileName);
+typedef void (*RecoveryShutdownCB) (void);
+
+typedef struct RecoveryModuleCallbacks
+{
+	RecoveryRestoreCB restore_cb;
+	RecoveryArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndCB recovery_end_cb;
+	RecoveryShutdownCB shutdown_cb;
+} RecoveryModuleCallbacks;
+
+extern RecoveryModuleCallbacks RecoveryContext;
+
+/*
+ * Type of the shared library symbol _PG_recovery_module_init that is looked up
+ * when loading a recovery library.
+ */
+typedef void (*RecoveryModuleInit) (RecoveryModuleCallbacks *cb);
+
+extern PGDLLEXPORT void _PG_recovery_module_init(RecoveryModuleCallbacks *cb);
+
+extern void LoadRecoveryCallbacks(void);
+extern void call_recovery_module_shutdown_cb(int code, Datum arg);
+
+/*
+ * Since the logic for recovery via a shell command is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern void shell_restore_init(RecoveryModuleCallbacks *cb);
 
 #endif							/* XLOG_ARCHIVE_H */
diff --git a/src/include/access/xlogrecovery.h b/src/include/access/xlogrecovery.h
index 47c29350f5..35d1d09374 100644
--- a/src/include/access/xlogrecovery.h
+++ b/src/include/access/xlogrecovery.h
@@ -55,6 +55,7 @@ extern PGDLLIMPORT int recovery_min_apply_delay;
 extern PGDLLIMPORT char *PrimaryConnInfo;
 extern PGDLLIMPORT char *PrimarySlotName;
 extern PGDLLIMPORT char *recoveryRestoreCommand;
+extern PGDLLIMPORT char *restoreLibrary;
 extern PGDLLIMPORT char *recoveryEndCommand;
 extern PGDLLIMPORT char *archiveCleanupCommand;
 
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index ba89d013e6..947597247f 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -404,6 +404,8 @@ extern void *guc_malloc(int elevel, size_t size);
 extern pg_nodiscard void *guc_realloc(int elevel, void *old, size_t size);
 extern char *guc_strdup(int elevel, const char *src);
 extern void guc_free(void *ptr);
+extern void CheckMutuallyExclusiveGUCs(const char *p1val, const char *p1name,
+									   const char *p2val, const char *p2name);
 
 #ifdef EXEC_BACKEND
 extern void write_nondefault_variables(GucContext context);
-- 
2.25.1


--UugvWAfsgieZRqgk--





^ permalink  raw  reply  [nested|flat] 17+ messages in thread

* [PATCH v7 2/2] Allow recovery via loadable modules.
@ 2022-12-10 03:40 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 17+ messages in thread

From: Nathan Bossart @ 2022-12-10 03:40 UTC (permalink / raw)

This adds the restore_library parameter to allow archive recovery
via a loadable module, rather than running shell commands.
---
 contrib/basic_archive/Makefile                |   4 +-
 contrib/basic_archive/basic_archive.c         |  67 ++++++-
 contrib/basic_archive/meson.build             |   7 +-
 contrib/basic_archive/t/001_restore.pl        |  44 +++++
 doc/src/sgml/archive-modules.sgml             | 168 ++++++++++++++++--
 doc/src/sgml/backup.sgml                      |  43 ++++-
 doc/src/sgml/basic-archive.sgml               |  33 ++--
 doc/src/sgml/config.sgml                      |  54 +++++-
 doc/src/sgml/high-availability.sgml           |  23 ++-
 src/backend/access/transam/shell_restore.c    |  21 ++-
 src/backend/access/transam/xlog.c             |  13 +-
 src/backend/access/transam/xlogarchive.c      |  70 +++++++-
 src/backend/access/transam/xlogrecovery.c     |  26 ++-
 src/backend/postmaster/checkpointer.c         |  26 +++
 src/backend/postmaster/pgarch.c               |   7 +-
 src/backend/postmaster/startup.c              |  23 ++-
 src/backend/utils/misc/guc.c                  |  14 ++
 src/backend/utils/misc/guc_tables.c           |  10 ++
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/access/xlog_internal.h            |   1 +
 src/include/access/xlogarchive.h              |  44 ++++-
 src/include/access/xlogrecovery.h             |   1 +
 src/include/utils/guc.h                       |   2 +
 23 files changed, 618 insertions(+), 84 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 55d299d650..487dc563f3 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -1,7 +1,7 @@
 # contrib/basic_archive/Makefile
 
 MODULES = basic_archive
-PGFILEDESC = "basic_archive - basic archive module"
+PGFILEDESC = "basic_archive - basic archive and recovery module"
 
 REGRESS = basic_archive
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.conf
@@ -9,6 +9,8 @@ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.c
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
 
+TAP_TESTS = 1
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index 28cbb6cce0..8c333c8f99 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
@@ -30,6 +35,7 @@
 #include <sys/time.h>
 #include <unistd.h>
 
+#include "access/xlogarchive.h"
 #include "common/int.h"
 #include "miscadmin.h"
 #include "postmaster/pgarch.h"
@@ -48,6 +54,8 @@ static bool basic_archive_file(const char *file, const char *path);
 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 bool basic_restore_file(const char *file, const char *path,
+							   const char *lastRestartPointFileName);
 
 /*
  * _PG_init
@@ -87,6 +95,19 @@ _PG_archive_module_init(ArchiveModuleCallbacks *cb)
 	cb->archive_file_cb = basic_archive_file;
 }
 
+/*
+ * _PG_recovery_module_init
+ *
+ * Returns the module's restore callback.
+ */
+void
+_PG_recovery_module_init(RecoveryModuleCallbacks *cb)
+{
+	AssertVariableIsOfType(&_PG_recovery_module_init, RecoveryModuleInit);
+
+	cb->restore_cb = basic_restore_file;
+}
+
 /*
  * check_archive_directory
  *
@@ -99,8 +120,8 @@ check_archive_directory(char **newval, void **extra, GucSource source)
 
 	/*
 	 * The default value is an empty string, so we have to accept that value.
-	 * Our check_configured callback also checks for this and prevents
-	 * archiving from proceeding if it is still empty.
+	 * Our check_configured and restore callbacks also check for this and
+	 * prevent archiving or recovery from proceeding if it is still empty.
 	 */
 	if (*newval == NULL || *newval[0] == '\0')
 		return true;
@@ -368,3 +389,45 @@ compare_files(const char *file1, const char *file2)
 
 	return ret;
 }
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file from the WAL archives.
+ */
+static bool
+basic_restore_file(const char *file, const char *path,
+				   const char *lastRestartPointFileName)
+{
+	char		source[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive", file)));
+
+	if (archive_directory == NULL || archive_directory[0] == '\0')
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"basic_archive.archive_directory\" is not set")));
+
+	/*
+	 * Check whether the file exists.  If not, we return false to indicate that
+	 * there are no more files to restore.
+	 */
+	snprintf(source, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(source, &st) != 0)
+	{
+		int		elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m", source)));
+		return false;
+	}
+
+	copy_file(source, unconstify(char *, path));
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive", file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index bc1380e6f6..af4580dea9 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -7,7 +7,7 @@ basic_archive_sources = files(
 if host_system == 'windows'
   basic_archive_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
     '--NAME', 'basic_archive',
-    '--FILEDESC', 'basic_archive - basic archive module',])
+    '--FILEDESC', 'basic_archive - basic archive and recovery module',])
 endif
 
 basic_archive = shared_module('basic_archive',
@@ -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..ec8767d740
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,44 @@
+
+# Copyright (c) 2022, 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-modules.sgml b/doc/src/sgml/archive-modules.sgml
index ef02051f7f..53e657040b 100644
--- a/doc/src/sgml/archive-modules.sgml
+++ b/doc/src/sgml/archive-modules.sgml
@@ -1,34 +1,40 @@
 <!-- doc/src/sgml/archive-modules.sgml -->
 
 <chapter id="archive-modules">
- <title>Archive Modules</title>
+ <title>Archive and Recovery Modules</title>
  <indexterm zone="archive-modules">
-  <primary>Archive Modules</primary>
+  <primary>Archive and Recovery Modules</primary>
  </indexterm>
 
  <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 (e.g., <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>
   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"/>.
+  were successfully archived.  When a custom
+  <xref linkend="guc-restore-library"/> is configured, PostgreSQL will use the
+  module for recovery actions.  It is ultimately up to the module to decide how
+  to accomplish each task, but some recommendations are listed at
+  <xref linkend="backup-archiving-wal"/> and
+  <xref linkend="backup-pitr-recovery"/>.
  </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).
+  Archive and recovery modules must at least consist of an initialization
+  function (see <xref linkend="archive-module-init"/> and
+  <xref linkend="recovery-module-init"/>) and the required callbacks (see
+  <xref linkend="archive-module-callbacks"/> and
+  <xref linkend="recovery-module-callbacks"/>).  However, archive and recovery
+  modules are also permitted to do much more (e.g., declare GUCs and register
+  background workers).  A module may be used for both
+  <varname>archive_library</varname> and <varname>restore_library</varname>.
  </para>
 
  <para>
@@ -37,7 +43,7 @@
  </para>
 
  <sect1 id="archive-module-init">
-  <title>Initialization Functions</title>
+  <title>Archive Module Initialization Functions</title>
   <indexterm zone="archive-module-init">
    <primary>_PG_archive_module_init</primary>
   </indexterm>
@@ -64,6 +70,12 @@ typedef void (*ArchiveModuleInit) (struct ArchiveModuleCallbacks *cb);
    Only the <function>archive_file_cb</function> callback is required.  The
    others are optional.
   </para>
+
+  <note>
+   <para>
+    <varname>archive_library</varname> is only loaded in the archiver process.
+   </para>
+  </note>
  </sect1>
 
  <sect1 id="archive-module-callbacks">
@@ -129,6 +141,132 @@ typedef bool (*ArchiveFileCB) (const char *file, const char *path);
 
 <programlisting>
 typedef void (*ArchiveShutdownCB) (void);
+</programlisting>
+   </para>
+  </sect2>
+ </sect1>
+
+ <sect1 id="recovery-module-init">
+  <title>Recovery Module Initialization Functions</title>
+  <indexterm zone="recovery-module-init">
+   <primary>_PG_recovery_module_init</primary>
+  </indexterm>
+  <para>
+   A recovery library is loaded by dynamically loading a shared library with the
+   <xref linkend="guc-restore-library"/> as the library base name.  The normal
+   library search path is used to locate the library.  To provide the required
+   recovery module callbacks and to indicate that the library is actually a
+   recovery module, it needs to provide a function named
+   <function>_PG_recovery_module_init</function>.  This function is passed a
+   struct that needs to be filled with the callback function pointers for
+   individual actions.
+
+<programlisting>
+typedef struct RecoveryModuleCallbacks
+{
+    RecoveryRestoreCB restore_cb;
+    RecoveryArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndCB recovery_end_cb;
+    RecoveryShutdownCB shutdown_cb;
+} RecoveryModuleCallbacks;
+typedef void (*RecoveryModuleInit) (struct RecoveryModuleCallbacks *cb);
+</programlisting>
+
+   The <function>restore_cb</function> callback is required for archive
+   recovery, but it is optional for streaming replication.  The others are
+   always optional.
+  </para>
+
+  <note>
+   <para>
+    <varname>restore_library</varname> is only loaded in the startup and
+    checkpointer processes and in single-user mode.
+   </para>
+  </note>
+ </sect1>
+
+ <sect1 id="recovery-module-callbacks">
+  <title>Recovery Module Callbacks</title>
+  <para>
+   The recovery callbacks define the actual behavior of the module.  The server
+   will call them as required to execute recovery actions.
+  </para>
+
+  <sect2 id="recovery-module-restore">
+   <title>Restore Callback</title>
+   <para>
+    The <function>restore_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 (*RecoveryRestoreCB) (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>
+  </sect2>
+
+  <sect2 id="recovery-module-archive-cleanup">
+   <title>Archive Cleanup Callback</title>
+   <para>
+    The <function>archive_cleanup_cb</function> callback is called at every
+    restart point 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 (*RecoveryArchiveCleanupCB) (const char *lastRestartPointFileName);
+</programlisting>
+
+    <replaceable>lastRestartPointFileName</replaceable> will contain the name
+    of the file containing the last valid restart point, like in
+    <link linkend="recovery-module-restore"><function>restore_cb</function></link>.
+   </para>
+  </sect2>
+
+  <sect2 id="recovery-module-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
+    replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (const char *lastRestartPointFileName);
+</programlisting>
+
+    <replaceable>lastRestartPointFileName</replaceable> will contain the name
+    of the file containing the last valid restart point, like in
+    <link linkend="recovery-module-restore"><function>restore_cb</function></link>.
+   </para>
+  </sect2>
+
+  <sect2 id="recovery-module-shutdown">
+   <title>Shutdown Callback</title>
+   <para>
+    The <function>shutdown_cb</function> callback is called when a process that
+    has loaded the recovery 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.
+
+<programlisting>
+typedef void (*RecoveryShutdownCB) (void);
 </programlisting>
    </para>
   </sect2>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index be05a33205..f44135061d 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1180,9 +1180,27 @@ 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>,
-    which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>
+    that defines a restore callback, which tells
+    <productname>PostgreSQL</productname> how to retrieve archived 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,
+    recovery modules can be more performant 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="archive-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 +1219,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 function returns
+    <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 function provided by the
+    <varname>restore_library</varname> emits an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1232,7 +1256,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 60f23d2855..11fd670dbc 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 recovery
+  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 recovery modules.  For
+  more information about archive and recovery modules, see
+  see <xref linkend="archive-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 recovery 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,7 +65,8 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
-   Server crashes may leave temporary files with the prefix
+   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
    remove such files while the server is running as long as they are unrelated
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 77574e2d4e..039d3360ac 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3773,7 +3773,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
@@ -3801,7 +3802,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,
@@ -3836,7 +3838,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 recovery actions, including retrieving archived
+        segments of the WAL file series 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 recovery.
+        For more information, see <xref linkend="archive-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>
@@ -3881,7 +3918,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>
@@ -3910,11 +3950,13 @@ 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>
-
     </variablelist>
 
   </sect2>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index f180607528..6266e2df7f 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,9 +639,11 @@ 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
-    reaches the end of WAL available there and <varname>restore_command</varname>
-    fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s restore callback.
+    Once it reaches the end of WAL available there and
+    <varname>restore_command</varname> or the restore callback 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
     from the last valid record found in archive or <filename>pg_wal</filename>. If that fails
@@ -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 restore callbacks provided by
+     <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,10 @@ 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
+    <function>archive_cleanup_cb</function> callback function 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/shell_restore.c b/src/backend/access/transam/shell_restore.c
index f5b6cf174e..c0bc78d8b4 100644
--- a/src/backend/access/transam/shell_restore.c
+++ b/src/backend/access/transam/shell_restore.c
@@ -4,7 +4,8 @@
  *		Recovery functions for a user-specified shell command.
  *
  * These recovery functions use a user-specified shell command (e.g. based
- * on the GUC restore_command).
+ * on the GUC restore_command).  It is used as the default, but other
+ * modules may define their own recovery logic.
  *
  * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
  * Portions Copyright (c) 1994, Regents of the University of California
@@ -25,11 +26,25 @@
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static bool shell_restore(const char *file, const char *path,
+						  const char *lastRestartPointFileName);
+static void shell_archive_cleanup(const char *lastRestartPointFileName);
+static void shell_recovery_end(const char *lastRestartPointFileName);
 static bool ExecuteRecoveryCommand(const char *command,
 								   const char *commandName,
 								   bool failOnSignal, bool exitOnSigterm,
 								   uint32 wait_event_info, int fail_elevel);
 
+void
+shell_restore_init(RecoveryModuleCallbacks *cb)
+{
+	AssertVariableIsOfType(&shell_restore_init, RecoveryModuleInit);
+
+	cb->restore_cb = shell_restore;
+	cb->archive_cleanup_cb = shell_archive_cleanup;
+	cb->recovery_end_cb = shell_recovery_end;
+}
+
 /*
  * Attempt to execute a shell-based restore command.
  *
@@ -80,7 +95,7 @@ shell_restore(const char *file, const char *path,
 /*
  * Attempt to execute a shell-based archive cleanup command.
  */
-void
+static void
 shell_archive_cleanup(const char *lastRestartPointFileName)
 {
 	char	   *cmd;
@@ -96,7 +111,7 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 /*
  * Attempt to execute a shell-based end-of-recovery command.
  */
-void
+static void
 shell_recovery_end(const char *lastRestartPointFileName)
 {
 	char	   *cmd;
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 8f47fb7570..ae537cd87f 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4884,15 +4884,16 @@ static void
 CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
+
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 */
-	if (recoveryEndCommand && strcmp(recoveryEndCommand, "") != 0)
+	if (RecoveryContext.recovery_end_cb)
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_recovery_end(lastRestartPointFname);
+		RecoveryContext.recovery_end_cb(lastRestartPointFname);
 	}
 
 	/*
@@ -7307,14 +7308,14 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Execute the archive-cleanup callback, if any.
 	 */
-	if (archiveCleanupCommand && strcmp(archiveCleanupCommand, "") != 0)
+	if (RecoveryContext.archive_cleanup_cb)
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_archive_cleanup(lastRestartPointFname);
+		RecoveryContext.archive_cleanup_cb(lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index b5cb060d55..fdab7dad43 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -22,7 +22,9 @@
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
+#include "access/xlogrecovery.h"
 #include "common/archive.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/startup.h"
@@ -32,6 +34,11 @@
 #include "storage/ipc.h"
 #include "storage/lwlock.h"
 
+/*
+ * Global context for recovery-related callbacks.
+ */
+RecoveryModuleCallbacks RecoveryContext;
+
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
  * If successful, fill "path" with its complete path (note that this will be
@@ -71,7 +78,7 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 		goto not_available;
 
 	/* In standby mode, restore_command might not be supplied */
-	if (recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0)
+	if (RecoveryContext.restore_cb == NULL)
 		goto not_available;
 
 	/*
@@ -149,14 +156,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 		XLogFileName(lastRestartPointFname, 0, 0L, wal_segment_size);
 
 	/*
-	 * Check signals before restore command and reset afterwards.
+	 * Check signals before restore callback and reset afterwards.
 	 */
 	PreRestoreCommand();
 
 	/*
 	 * Copy xlog from archival storage to XLOGDIR
 	 */
-	ret = shell_restore(xlogfname, xlogpath, lastRestartPointFname);
+	ret = RecoveryContext.restore_cb(xlogfname, xlogpath,
+									 lastRestartPointFname);
 
 	PostRestoreCommand();
 
@@ -603,3 +611,59 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the recovery callbacks into our global RecoveryContext.  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
+LoadRecoveryCallbacks(void)
+{
+	RecoveryModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_recovery_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RecoveryModuleInit)
+			load_external_function(restoreLibrary, "_PG_recovery_module_init",
+								   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("recovery modules have to define the symbol "
+						"_PG_recovery_module_init")));
+
+	memset(&RecoveryContext, 0, sizeof(RecoveryModuleCallbacks));
+	(*init) (&RecoveryContext);
+
+	/*
+	 * If using shell commands, remove callbacks for any commands that are not
+	 * set.
+	 */
+	if (restoreLibrary[0] == '\0')
+	{
+		if (recoveryRestoreCommand[0] == '\0')
+			RecoveryContext.restore_cb = NULL;
+		if (archiveCleanupCommand[0] == '\0')
+			RecoveryContext.archive_cleanup_cb = NULL;
+		if (recoveryEndCommand[0] == '\0')
+			RecoveryContext.recovery_end_cb = NULL;
+	}
+}
+
+/*
+ * Call the shutdown callback of the loaded recovery module, if defined.
+ */
+void
+call_recovery_module_shutdown_cb(int code, Datum arg)
+{
+	if (RecoveryContext.shutdown_cb)
+		RecoveryContext.shutdown_cb();
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 5e65785306..db0cd4469a 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -80,6 +80,7 @@ const struct config_enum_entry recovery_target_action_options[] = {
 
 /* options formerly taken from recovery.conf for archive recovery */
 char	   *recoveryRestoreCommand = NULL;
+char	   *restoreLibrary = NULL;
 char	   *recoveryEndCommand = NULL;
 char	   *archiveCleanupCommand = NULL;
 RecoveryTargetType recoveryTarget = RECOVERY_TARGET_UNSET;
@@ -1053,24 +1054,37 @@ validateRecoveryParameters(void)
 	if (!ArchiveRecoveryRequested)
 		return;
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryRestoreCommand, "restore_command");
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryEndCommand, "recovery_end_command");
+	before_shmem_exit(call_recovery_module_shutdown_cb, 0);
+	LoadRecoveryCallbacks();
+
 	/*
 	 * Check for compulsory parameters
 	 */
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			RecoveryContext.restore_cb == NULL)
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
-					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
+					(errmsg("specified neither primary_conninfo nor restore_command "
+							"nor a restore_library that defines a restore callback"),
+					 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 (RecoveryContext.restore_cb == NULL)
 			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 restore_library that defines "
+							"a restore callback when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index de0bbbfa79..6350fd0b83 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -38,6 +38,7 @@
 
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
+#include "access/xlogarchive.h"
 #include "access/xlogrecovery.h"
 #include "libpq/pqsignal.h"
 #include "miscadmin.h"
@@ -222,6 +223,16 @@ CheckpointerMain(void)
 	 */
 	before_shmem_exit(pgstat_before_server_shutdown, 0);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.  We do this before setting up the exception handler
+	 * so that any problems result in a server crash shortly after startup.
+	 */
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   archiveCleanupCommand, "archive_cleanup_command");
+	before_shmem_exit(call_recovery_module_shutdown_cb, 0);
+	LoadRecoveryCallbacks();
+
 	/*
 	 * Create a memory context that we will do all our work in.  We do this so
 	 * that we can reset the context during error recovery and thereby avoid
@@ -548,6 +559,9 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -563,6 +577,18 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+								   archiveCleanupCommand, "archive_cleanup_command");
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			call_recovery_module_shutdown_cb(0, (Datum) 0);
+			LoadRecoveryCallbacks();
+		}
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c
index 8ecdb9ca23..8e91f2d70f 100644
--- a/src/backend/postmaster/pgarch.c
+++ b/src/backend/postmaster/pgarch.c
@@ -831,11 +831,8 @@ 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.")));
+	CheckMutuallyExclusiveGUCs(XLogArchiveLibrary, "archive_library",
+							   XLogArchiveCommand, "archive_command");
 
 	memset(&ArchiveContext, 0, sizeof(ArchiveModuleCallbacks));
 
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index 8786186898..f9ff2b5583 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -20,6 +20,7 @@
 #include "postgres.h"
 
 #include "access/xlog.h"
+#include "access/xlogarchive.h"
 #include "access/xlogrecovery.h"
 #include "access/xlogutils.h"
 #include "libpq/pqsignal.h"
@@ -133,13 +134,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 recovery 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;
@@ -161,6 +166,22 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryRestoreCommand, "restore_command");
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryEndCommand, "recovery_end_command");
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_recovery_module_shutdown_cb(0, (Datum) 0);
+		LoadRecoveryCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index d52069f446..7858e9a649 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -6880,3 +6880,17 @@ call_enum_check_hook(struct config_enum *conf, int *newval, void **extra,
 
 	return true;
 }
+
+/*
+ * ERROR if both parameters are set.
+ */
+void
+CheckMutuallyExclusiveGUCs(const char *p1val, const char *p1name,
+						   const char *p2val, const char *p2name)
+{
+	if (p1val[0] != '\0' && p2val[0] != '\0')
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("both %s and %s set", p1name, p2name),
+				 errdetail("Only one of %s, %s may be set.", p1name, p2name)));
+}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 5025e80f89..a8a516b0c6 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -3776,6 +3776,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 recovery 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 4cceda4162..38fb3e0823 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -269,6 +269,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 recovery 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/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 59fc7bc105..756f0898b5 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -400,5 +400,6 @@ extern PGDLLIMPORT bool ArchiveRecoveryRequested;
 extern PGDLLIMPORT bool InArchiveRecovery;
 extern PGDLLIMPORT bool StandbyMode;
 extern PGDLLIMPORT char *recoveryRestoreCommand;
+extern PGDLLIMPORT char *restoreLibrary;
 
 #endif							/* XLOG_INTERNAL_H */
diff --git a/src/include/access/xlogarchive.h b/src/include/access/xlogarchive.h
index 299304703e..71c9b88165 100644
--- a/src/include/access/xlogarchive.h
+++ b/src/include/access/xlogarchive.h
@@ -30,9 +30,45 @@ extern bool XLogArchiveIsReady(const char *xlog);
 extern bool XLogArchiveIsReadyOrDone(const char *xlog);
 extern void XLogArchiveCleanup(const char *xlog);
 
-extern bool shell_restore(const char *file, const char *path,
-						  const char *lastRestartPointFileName);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Recovery module callbacks
+ *
+ * These callback functions should be defined by recovery libraries and
+ * returned via _PG_recovery_module_init().  For more information about the
+ * purpose of each callback, refer to the recovery modules documentation.
+ */
+typedef bool (*RecoveryRestoreCB) (const char *file, const char *path,
+								   const char *lastRestartPointFileName);
+typedef void (*RecoveryArchiveCleanupCB) (const char *lastRestartPointFileName);
+typedef void (*RecoveryEndCB) (const char *lastRestartPointFileName);
+typedef void (*RecoveryShutdownCB) (void);
+
+typedef struct RecoveryModuleCallbacks
+{
+	RecoveryRestoreCB restore_cb;
+	RecoveryArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndCB recovery_end_cb;
+	RecoveryShutdownCB shutdown_cb;
+} RecoveryModuleCallbacks;
+
+extern RecoveryModuleCallbacks RecoveryContext;
+
+/*
+ * Type of the shared library symbol _PG_recovery_module_init that is looked up
+ * when loading a recovery library.
+ */
+typedef void (*RecoveryModuleInit) (RecoveryModuleCallbacks *cb);
+
+extern PGDLLEXPORT void _PG_recovery_module_init(RecoveryModuleCallbacks *cb);
+
+extern void LoadRecoveryCallbacks(void);
+extern void call_recovery_module_shutdown_cb(int code, Datum arg);
+
+/*
+ * Since the logic for recovery via a shell command is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern void shell_restore_init(RecoveryModuleCallbacks *cb);
 
 #endif							/* XLOG_ARCHIVE_H */
diff --git a/src/include/access/xlogrecovery.h b/src/include/access/xlogrecovery.h
index 47c29350f5..35d1d09374 100644
--- a/src/include/access/xlogrecovery.h
+++ b/src/include/access/xlogrecovery.h
@@ -55,6 +55,7 @@ extern PGDLLIMPORT int recovery_min_apply_delay;
 extern PGDLLIMPORT char *PrimaryConnInfo;
 extern PGDLLIMPORT char *PrimarySlotName;
 extern PGDLLIMPORT char *recoveryRestoreCommand;
+extern PGDLLIMPORT char *restoreLibrary;
 extern PGDLLIMPORT char *recoveryEndCommand;
 extern PGDLLIMPORT char *archiveCleanupCommand;
 
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index ba89d013e6..947597247f 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -404,6 +404,8 @@ extern void *guc_malloc(int elevel, size_t size);
 extern pg_nodiscard void *guc_realloc(int elevel, void *old, size_t size);
 extern char *guc_strdup(int elevel, const char *src);
 extern void guc_free(void *ptr);
+extern void CheckMutuallyExclusiveGUCs(const char *p1val, const char *p1name,
+									   const char *p2val, const char *p2name);
 
 #ifdef EXEC_BACKEND
 extern void write_nondefault_variables(GucContext context);
-- 
2.25.1


--PEIAKu/WMn1b1Hv9--





^ permalink  raw  reply  [nested|flat] 17+ messages in thread

* [PATCH v1 3/3] Allow recovery via loadable modules.
@ 2022-12-10 03:40 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 17+ messages in thread

From: Nathan Bossart @ 2022-12-10 03:40 UTC (permalink / raw)

This adds the restore_library, archive_cleanup_library, and
recovery_end_library parameters to allow archive recovery via a
loadable module, rather than running shell commands.
---
 contrib/basic_archive/Makefile                |   4 +-
 contrib/basic_archive/basic_archive.c         |  69 ++++++-
 contrib/basic_archive/meson.build             |   7 +-
 contrib/basic_archive/t/001_restore.pl        |  42 +++++
 doc/src/sgml/archive-modules.sgml             | 176 ++++++++++++++++--
 doc/src/sgml/backup.sgml                      |  40 +++-
 doc/src/sgml/basic-archive.sgml               |  33 ++--
 doc/src/sgml/config.sgml                      |  99 +++++++++-
 doc/src/sgml/high-availability.sgml           |  18 +-
 src/backend/access/transam/shell_restore.c    |  21 ++-
 src/backend/access/transam/xlog.c             |  13 +-
 src/backend/access/transam/xlogarchive.c      | 131 ++++++++++++-
 src/backend/access/transam/xlogrecovery.c     |  20 +-
 src/backend/postmaster/checkpointer.c         |  23 +++
 src/backend/postmaster/startup.c              |  22 ++-
 src/backend/utils/misc/guc_tables.c           |  30 +++
 src/backend/utils/misc/postgresql.conf.sample |   3 +
 src/include/access/xlog_internal.h            |   1 +
 src/include/access/xlogarchive.h              |  43 ++++-
 src/include/access/xlogrecovery.h             |   3 +
 20 files changed, 724 insertions(+), 74 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 55d299d650..487dc563f3 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -1,7 +1,7 @@
 # contrib/basic_archive/Makefile
 
 MODULES = basic_archive
-PGFILEDESC = "basic_archive - basic archive module"
+PGFILEDESC = "basic_archive - basic archive and recovery module"
 
 REGRESS = basic_archive
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.conf
@@ -9,6 +9,8 @@ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.c
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
 
+TAP_TESTS = 1
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index 9f221816bb..2a702455ca 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, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
@@ -30,6 +35,7 @@
 #include <sys/time.h>
 #include <unistd.h>
 
+#include "access/xlogarchive.h"
 #include "common/int.h"
 #include "miscadmin.h"
 #include "postmaster/pgarch.h"
@@ -48,6 +54,8 @@ static bool basic_archive_file(const char *file, const char *path);
 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 bool basic_restore_file(const char *file, const char *path,
+							   const char *lastRestartPointFileName);
 
 /*
  * _PG_init
@@ -87,6 +95,19 @@ _PG_archive_module_init(ArchiveModuleCallbacks *cb)
 	cb->archive_file_cb = basic_archive_file;
 }
 
+/*
+ * _PG_recovery_module_init
+ *
+ * Returns the module's restore callback.
+ */
+void
+_PG_recovery_module_init(RecoveryModuleCallbacks *cb)
+{
+	AssertVariableIsOfType(&_PG_recovery_module_init, RecoveryModuleInit);
+
+	cb->restore_cb = basic_restore_file;
+}
+
 /*
  * check_archive_directory
  *
@@ -99,8 +120,8 @@ check_archive_directory(char **newval, void **extra, GucSource source)
 
 	/*
 	 * The default value is an empty string, so we have to accept that value.
-	 * Our check_configured callback also checks for this and prevents
-	 * archiving from proceeding if it is still empty.
+	 * Our check_configured and restore callbacks also check for this and
+	 * prevent archiving or recovery from proceeding if it is still empty.
 	 */
 	if (*newval == NULL || *newval[0] == '\0')
 		return true;
@@ -368,3 +389,47 @@ compare_files(const char *file1, const char *file2)
 
 	return ret;
 }
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file from the WAL archives.
+ */
+static bool
+basic_restore_file(const char *file, const char *path,
+				   const char *lastRestartPointFileName)
+{
+	char		source[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive", file)));
+
+	if (archive_directory == NULL || archive_directory[0] == '\0')
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"basic_archive.archive_directory\" is not set")));
+
+	/*
+	 * Check whether the file exists.  If not, we return false to indicate that
+	 * there are no more files to restore.
+	 */
+	snprintf(source, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(source, &st) != 0)
+	{
+		int		elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m", source)));
+		return false;
+	}
+
+	copy_file(source, unconstify(char *, path));
+	fsync_fname(path, false);
+	fsync_fname(archive_directory, true);
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive", file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index b25dce99a3..5b5a6d79e7 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -7,7 +7,7 @@ basic_archive_sources = files(
 if host_system == 'windows'
   basic_archive_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
     '--NAME', 'basic_archive',
-    '--FILEDESC', 'basic_archive - basic archive module',])
+    '--FILEDESC', 'basic_archive - basic archive and recovery module',])
 endif
 
 basic_archive = shared_module('basic_archive',
@@ -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..86ff0dc7f5
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,42 @@
+
+# Copyright (c) 2022, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+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;
+$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-modules.sgml b/doc/src/sgml/archive-modules.sgml
index ef02051f7f..023f2e047a 100644
--- a/doc/src/sgml/archive-modules.sgml
+++ b/doc/src/sgml/archive-modules.sgml
@@ -1,34 +1,43 @@
 <!-- doc/src/sgml/archive-modules.sgml -->
 
 <chapter id="archive-modules">
- <title>Archive Modules</title>
+ <title>Archive and Recovery Modules</title>
  <indexterm zone="archive-modules">
-  <primary>Archive Modules</primary>
+  <primary>Archive and Recovery Modules</primary>
  </indexterm>
 
  <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 (e.g., <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>
   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"/>.
+  were successfully archived.  When a custom
+  <xref linkend="guc-restore-library"/>,
+  <xref linkend="guc-archive-cleanup-library"/>, or
+  <xref linkend="guc-recovery-end-library"/> is configured, PostgreSQL will use
+  the module for the corresponding recovery action.  It is ultimately up to the
+  module to decide how to accomplish each task, but some recommendations are
+  listed at <xref linkend="backup-archiving-wal"/> and
+  <xref linkend="backup-pitr-recovery"/>.
  </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).
+  Archive and recovery modules must at least consist of an initialization
+  function (see <xref linkend="archive-module-init"/> and
+  <xref linkend="recovery-module-init"/>) and the required callbacks (see
+  <xref linkend="archive-module-callbacks"/> and
+  <xref linkend="recovery-module-callbacks"/>).  However, archive and recovery
+  modules are also permitted to do much more (e.g., declare GUCs and register
+  background workers).  A module may be used for any combination of the
+  configuration parameters mentioned in the preceding paragraph, or it can be
+  used for just one.
  </para>
 
  <para>
@@ -37,7 +46,7 @@
  </para>
 
  <sect1 id="archive-module-init">
-  <title>Initialization Functions</title>
+  <title>Archive Module Initialization Functions</title>
   <indexterm zone="archive-module-init">
    <primary>_PG_archive_module_init</primary>
   </indexterm>
@@ -64,6 +73,12 @@ typedef void (*ArchiveModuleInit) (struct ArchiveModuleCallbacks *cb);
    Only the <function>archive_file_cb</function> callback is required.  The
    others are optional.
   </para>
+
+  <note>
+   <para>
+    <varname>archive_library</varname> is only loaded in the archiver process.
+   </para>
+  </note>
  </sect1>
 
  <sect1 id="archive-module-callbacks">
@@ -133,4 +148,135 @@ typedef void (*ArchiveShutdownCB) (void);
    </para>
   </sect2>
  </sect1>
+
+ <sect1 id="recovery-module-init">
+  <title>Recovery Module Initialization Functions</title>
+  <indexterm zone="recovery-module-init">
+   <primary>_PG_recovery_module_init</primary>
+  </indexterm>
+  <para>
+   A recovery library is loaded by dynamically loading a shared library with the
+   <xref linkend="guc-restore-library"/>,
+   <xref linkend="guc-archive-cleanup-library"/>, or
+   <xref linkend="guc-recovery-end-library"/> as the library base name.  The
+   normal library search path is used to locate the library.  To provide the
+   required recovery module callbacks and to indicate that the library is
+   actually a recovery module, it needs to provide a function named
+   <function>_PG_recovery_module_init</function>.  This function is passed a
+   struct that needs to be filled with the callback function pointers for
+   individual actions.
+
+<programlisting>
+typedef struct RecoveryModuleCallbacks
+{
+    RecoveryRestoreCB restore_cb;
+    RecoveryArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndCB recovery_end_cb;
+} RecoveryModuleCallbacks;
+typedef void (*RecoveryModuleInit) (struct RecoveryModuleCallbacks *cb);
+</programlisting>
+
+   If the recovery module is loaded via <varname>restore_library</varname>, the
+   <function>restore_cb</function> callback is required.  If the recovery
+   module is loaded via <varname>archive_cleanup_library</varname>, the
+   <function>archive_cleanup_cb</function> callback is required.  If the
+   recovery module is loaded via <varname>recovery_end_library</varname>, the
+   <function>recovery_end_library</function> callback is required.  The same
+   recovery module may be used for more than one of the aforementioned
+   parameters if desired.  Unused callback functions (e.g., if
+   <function>restore_cb</function> is defined but the library is only loaded
+   via <varname>recovery_end_library</varname>) are ignored.
+  </para>
+
+  <note>
+   <para>
+    <varname>restore_library</varname> and
+    <varname>recovery_end_library</varname> are only loaded in the startup
+    process and in single-user mode, while
+    <varname>archive_cleanup_library</varname> is only loaded in the
+    checkpointer process.
+   </para>
+  </note>
+
+  <note>
+   <para>
+    A recovery module's <function>_PG_recovery_module_init</function> might be
+    called multiple times in the same process.  If a module uses this function
+    for anything beyond returning its callback functions, it must be able to cope
+    with multiple invocations.
+   </para>
+  </note>
+ </sect1>
+
+ <sect1 id="recovery-module-callbacks">
+  <title>Recovery Module Callbacks</title>
+  <para>
+   The recovery callbacks define the actual behavior of the module.  The server
+   will call them as required to execute recovery actions.
+  </para>
+
+  <sect2 id="recovery-module-restore">
+   <title>Restore Callback</title>
+   <para>
+    The <function>restore_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 (*RecoveryRestoreCB) (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>
+  </sect2>
+
+  <sect2 id="recovery-module-archive-cleanup">
+   <title>Archive Cleanup Callback</title>
+   <para>
+    The <function>archive_cleanup_cb</function> callback is called at every
+    restart point 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 (*RecoveryArchiveCleanupCB) (const char *lastRestartPointFileName);
+</programlisting>
+
+    <replaceable>lastRestartPointFileName</replaceable> will contain the name
+    of the file containing the last valid restart point, like in
+    <link linkend="recovery-module-restore"><function>restore_cb</function></link>.
+   </para>
+  </sect2>
+
+  <sect2 id="recovery-module-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
+    replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (const char *lastRestartPointFileName);
+</programlisting>
+
+    <replaceable>lastRestartPointFileName</replaceable> will contain the name
+    of the file containing the last valid restart point, like in
+    <link linkend="recovery-module-restore"><function>restore_cb</function></link>.
+   </para>
+  </sect2>
+ </sect1>
 </chapter>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index 8bab521718..8cf3f35649 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1181,9 +1181,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 <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,
+    recovery modules can be more performant 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="archive-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.
@@ -1202,14 +1219,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 function returns
+    <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 function provided by the
+    <varname>restore_library</varname> emits an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1233,7 +1256,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 0b650f17a8..ac7cd9b967 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 recovery
+  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 recovery modules.  For
+  more information about archive and recovery modules, see
+  see <xref linkend="archive-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 recovery 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>
@@ -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,7 +65,8 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
-   Server crashes may leave temporary files with the prefix
+   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
    remove such files while the server is running as long as they are unrelated
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 3071c8eace..5cf9d0a42a 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3773,7 +3773,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
@@ -3801,7 +3802,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,
@@ -3836,7 +3838,36 @@ 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 retrieving an archived segment of the WAL file
+        series.  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> is used.  If both
+        <varname>restore_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
+        Otherwise, the specified shared library is used for restoring.  For
+        more information, see <xref linkend="archive-modules"/>.
+       </para>
+
+       <para>
+        This parameter can only be set at server start.
        </para>
       </listitem>
      </varlistentry>
@@ -3881,7 +3912,37 @@ 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>archive_cleanup_library</varname> is set to an empty string.
+        If both <varname>archive_cleanup_command</varname> and
+        <varname>archive_cleanup_library</varname> are set, an error will be
+        raised.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-archive-cleanup-library" xreflabel="archive_cleanup_library">
+      <term><varname>archive_cleanup_library</varname> (<type>string</type>)
+      <indexterm>
+        <primary><varname>archive_cleanup_library</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        This optional parameter specifies a library that will be executed at
+        every restartpoint.  The purpose of this parameter is to provide a
+        mechanism for cleaning up old archived WAL files that are no longer
+        needed by the standby server.  If this parameter is set to an empty
+        string (the default), the shell command specified in
+        <xref linkend="guc-archive-cleanup-command"/> is used.  If both
+        <varname>archive_cleanup_command</varname> and
+        <varname>archive_cleanup_library</varname> are set, an error will be
+        raised.  Otherwise, the specified shared library is used for cleanup.
+        For more information, see <xref linkend="archive-modules"/>.
+       </para>
+
+       <para>
+        This parameter can only be set at server start.
        </para>
       </listitem>
      </varlistentry>
@@ -3910,11 +3971,39 @@ 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>recovery_end_library</varname> is set to an empty string.  If
+        both <varname>recovery_end_command</varname> and
+        <varname>recovery_end_library</varname> are set, an error will be
+        raised.
        </para>
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-recovery-end-library" xreflabel="recovery_end_library">
+      <term><varname>recovery_end_library</varname> (<type>string</type>)
+      <indexterm>
+        <primary><varname>recovery_end_library</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        This optional parameter specifies a library that will be executed once
+        only at the end of recovery.  The purpose of this parameter is to
+        provide a mechanism for cleanup following replication or recovery.  If
+        this parameter is set to an empty string (the default), the shell
+        command specified in <xref linkend="guc-recovery-end-command"/> is
+        used.  If both <varname>recovery_end_command</varname> and
+        <varname>recovery_end_library</varname> are set, an error will be
+        raised.  Otherwise, the specified shared library is used for cleanup.
+        For more information, see <xref linkend="archive-modules"/>.
+       </para>
+
+       <para>
+        This parameter can only be set at server start.
+       </para>
+      </listitem>
+     </varlistentry>
     </variablelist>
 
   </sect2>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index f180607528..963d12e02a 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>. 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"/> or
+    <xref linkend="guc-archive-cleanup-library"/> parameter 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/shell_restore.c b/src/backend/access/transam/shell_restore.c
index 073e709e06..124a0bbdb6 100644
--- a/src/backend/access/transam/shell_restore.c
+++ b/src/backend/access/transam/shell_restore.c
@@ -3,7 +3,8 @@
  * shell_restore.c
  *
  * These recovery 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 recovery logic.
  *
  * Copyright (c) 2022, PostgreSQL Global Development Group
  *
@@ -22,6 +23,10 @@
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static bool shell_restore(const char *file, const char *path,
+						  const char *lastRestartPointFileName);
+static void shell_archive_cleanup(const char *lastRestartPointFileName);
+static void shell_recovery_end(const char *lastRestartPointFileName);
 static char *BuildCleanupCommand(const char *command,
 								 const char *lastRestartPointFileName);
 static bool ExecuteRecoveryCommand(const char *command,
@@ -29,6 +34,16 @@ static bool ExecuteRecoveryCommand(const char *command,
 								   bool exitOnSigterm, uint32 wait_event_info,
 								   int fail_elevel);
 
+void
+shell_restore_init(RecoveryModuleCallbacks *cb)
+{
+	AssertVariableIsOfType(&shell_restore_init, RecoveryModuleInit);
+
+	cb->restore_cb = shell_restore;
+	cb->archive_cleanup_cb = shell_archive_cleanup;
+	cb->recovery_end_cb = shell_recovery_end;
+}
+
 bool
 shell_restore(const char *file, const char *path,
 			  const char *lastRestartPointFileName)
@@ -73,7 +88,7 @@ shell_restore(const char *file, const char *path,
 	return ret;
 }
 
-void
+static void
 shell_archive_cleanup(const char *lastRestartPointFileName)
 {
 	char	   *cmd = BuildCleanupCommand(archiveCleanupCommand,
@@ -84,7 +99,7 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 	pfree(cmd);
 }
 
-void
+static void
 shell_recovery_end(const char *lastRestartPointFileName)
 {
 	char	   *cmd = BuildCleanupCommand(recoveryEndCommand,
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 32225be4a5..6365635180 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4883,10 +4883,11 @@ static void
 CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
+
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end library, if any.
 	 */
-	if (recoveryEndCommand && strcmp(recoveryEndCommand, "") != 0)
+	if (recoveryEndCommand[0] != '\0' || recoveryEndLibrary[0] != '\0')
 	{
 		char		lastRestartPointFname[MAXPGPATH];
 		XLogSegNo	restartSegNo;
@@ -4903,7 +4904,7 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 		XLogFileName(lastRestartPointFname, restartTli, restartSegNo,
 					 wal_segment_size);
 
-		shell_recovery_end(lastRestartPointFname);
+		RecoveryContext.recovery_end_cb(lastRestartPointFname);
 	}
 
 	/*
@@ -7318,9 +7319,9 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Execute the archive-cleanup library, if any.
 	 */
-	if (archiveCleanupCommand && strcmp(archiveCleanupCommand, "") != 0)
+	if (archiveCleanupCommand[0] != '\0' || archiveCleanupLibrary[0] != '\0')
 	{
 		char		lastRestartPointFname[MAXPGPATH];
 		XLogSegNo	restartSegNo;
@@ -7337,7 +7338,7 @@ CreateRestartPoint(int flags)
 		XLogFileName(lastRestartPointFname, restartTli, restartSegNo,
 					 wal_segment_size);
 
-		shell_archive_cleanup(lastRestartPointFname);
+		RecoveryContext.archive_cleanup_cb(lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index 50b0d1105d..574af1494e 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -22,7 +22,9 @@
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
+#include "access/xlogrecovery.h"
 #include "common/archive.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/startup.h"
@@ -32,6 +34,15 @@
 #include "storage/ipc.h"
 #include "storage/lwlock.h"
 
+/*
+ * Global context for recovery-related callbacks.
+ */
+RecoveryModuleCallbacks RecoveryContext;
+
+static void LoadRecoveryCallbacks(const char *cmd, const char *cmd_name,
+								  const char *lib, const char *lib_name,
+								  RecoveryModuleCallbacks *cb);
+
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
  * If successful, fill "path" with its complete path (note that this will be
@@ -71,7 +82,7 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 		goto not_available;
 
 	/* In standby mode, restore_command might not be supplied */
-	if (recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0)
+	if (recoveryRestoreCommand[0] == '\0' && recoveryRestoreLibrary[0] == '\0')
 		goto not_available;
 
 	/*
@@ -149,14 +160,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 		XLogFileName(lastRestartPointFname, 0, 0L, wal_segment_size);
 
 	/*
-	 * Check signals before restore command and reset afterwards.
+	 * Check signals before restore command/library and reset afterwards.
 	 */
 	PreRestoreCommand();
 
 	/*
 	 * Copy xlog from archival storage to XLOGDIR
 	 */
-	ret = shell_restore(xlogfname, xlogpath, lastRestartPointFname);
+	ret = RecoveryContext.restore_cb(xlogfname, xlogpath,
+									 lastRestartPointFname);
 
 	PostRestoreCommand();
 
@@ -603,3 +615,116 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Functions for loading recovery callbacks into the global RecoveryContext.
+ *
+ * To ensure that we only copy the necessary callbacks into the global context,
+ * we first copy them into a local context before copying the relevant one.
+ * This means that a recovery module's initialization function might be called
+ * multiple times in the same process.  If a module uses this function for
+ * anything beyond returning its callback functions, it must be able to cope
+ * with multiple invocations.
+ */
+
+/*
+ * Loads all the recovery callbacks for the command/library into cb.  This is
+ * intended for use by the functions below to load individual callbacks into
+ * the global RecoveryContext.
+ *
+ * If both the command and library are nonempty, an ERROR will be raised.
+ * cmd_name and lib_name are the GUC names to be used for the corresponding
+ * ERROR message.
+ */
+static void
+LoadRecoveryCallbacks(const char *cmd, const char *cmd_name, const char *lib,
+					  const char *lib_name, RecoveryModuleCallbacks *cb)
+{
+	RecoveryModuleInit init;
+
+	if (cmd[0] != '\0' && lib[0] != '\0')
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("both %s and %s set", cmd_name, lib_name),
+				 errdetail("Only one of %s, %s may be set.",
+						   cmd_name, lib_name)));
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_recovery_module_init().
+	 */
+	if (lib[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RecoveryModuleInit)
+			load_external_function(lib, "_PG_recovery_module_init",
+								   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("recovery modules have to define the symbol "
+						"_PG_recovery_module_init")));
+
+	(*init) (cb);
+}
+
+/*
+ * Loads only the restore callback into the global RecoveryContext.
+ */
+void
+LoadRestoreLibrary(void)
+{
+	RecoveryModuleCallbacks tmp = {0};
+
+	LoadRecoveryCallbacks(recoveryRestoreCommand, "restore_command",
+						  recoveryRestoreLibrary, "restore_library",
+						  &tmp);
+
+	if (tmp.restore_cb == NULL)
+		ereport(ERROR,
+				(errmsg("recovery modules used for \"restore_library\" must "
+						"register a restore callback")));
+	else
+		RecoveryContext.restore_cb = tmp.restore_cb;
+}
+
+/*
+ * Loads only the archive-cleanup callback into the global RecoveryContext.
+ */
+void
+LoadArchiveCleanupLibrary(void)
+{
+	RecoveryModuleCallbacks tmp = {0};
+
+	LoadRecoveryCallbacks(archiveCleanupCommand, "archive_cleanup_command",
+						  archiveCleanupLibrary, "archive_cleanup_library",
+						  &tmp);
+
+	if (tmp.archive_cleanup_cb == NULL)
+		ereport(ERROR,
+				(errmsg("recovery modules used for \"archive_cleanup_library\" "
+						"must register an archive cleanup callback")));
+	else
+		RecoveryContext.archive_cleanup_cb = tmp.archive_cleanup_cb;
+}
+
+/*
+ * Loads only the recovery-end callback into the global RecoveryContext.
+ */
+void
+LoadRecoveryEndLibrary(void)
+{
+	RecoveryModuleCallbacks tmp = {0};
+
+	LoadRecoveryCallbacks(recoveryEndCommand, "recovery_end_command",
+						  recoveryEndLibrary, "recovery_end_library",
+						  &tmp);
+
+	if (tmp.recovery_end_cb == NULL)
+		ereport(ERROR,
+				(errmsg("recovery modules used for \"recovery_end_library\" "
+						"must register a recovery end callback")));
+	else
+		RecoveryContext.recovery_end_cb = tmp.recovery_end_cb;
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index d5a81f9d83..d140054627 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -80,8 +80,11 @@ const struct config_enum_entry recovery_target_action_options[] = {
 
 /* options formerly taken from recovery.conf for archive recovery */
 char	   *recoveryRestoreCommand = NULL;
+char	   *recoveryRestoreLibrary = NULL;
 char	   *recoveryEndCommand = NULL;
+char	   *recoveryEndLibrary = NULL;
 char	   *archiveCleanupCommand = NULL;
+char	   *archiveCleanupLibrary = NULL;
 RecoveryTargetType recoveryTarget = RECOVERY_TARGET_UNSET;
 bool		recoveryTargetInclusive = true;
 int			recoveryTargetAction = RECOVERY_TARGET_ACTION_PAUSE;
@@ -1059,20 +1062,27 @@ validateRecoveryParameters(void)
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			recoveryRestoreCommand[0] == '\0' && recoveryRestoreLibrary[0] == '\0')
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
+					(errmsg("specified neither primary_conninfo nor restore_command nor 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 (recoveryRestoreCommand[0] == '\0' && recoveryRestoreLibrary[0] == '\0')
 			ereport(FATAL,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("must specify restore_command when standby mode is not enabled")));
+					 errmsg("must specify restore_command or restore_library when standby mode is not enabled")));
 	}
 
+	/*
+	 * Load the restore and recovery end libraries.  This also checks for
+	 * invalid combinations of the command/library parameters.
+	 */
+	memset(&RecoveryContext, 0, sizeof(RecoveryModuleCallbacks));
+	LoadRestoreLibrary();
+	LoadRecoveryEndLibrary();
+
 	/*
 	 * Override any inconsistent requests. Note that this is a change of
 	 * behaviour in 9.5; prior to this we simply ignored a request to pause if
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 5fc076fc14..9b479b41b4 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -38,6 +38,7 @@
 
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
+#include "access/xlogarchive.h"
 #include "access/xlogrecovery.h"
 #include "libpq/pqsignal.h"
 #include "miscadmin.h"
@@ -222,6 +223,15 @@ CheckpointerMain(void)
 	 */
 	before_shmem_exit(pgstat_before_server_shutdown, 0);
 
+	/*
+	 * Load the archive cleanup library.  This also checks that at most one of
+	 * archive_cleanup_command, archive_cleanup_library is set.  We do this
+	 * before setting up the exception handler so that any problems result in a
+	 * server crash shortly after startup.
+	 */
+	memset(&RecoveryContext, 0, sizeof(RecoveryModuleCallbacks));
+	LoadArchiveCleanupLibrary();
+
 	/*
 	 * Create a memory context that we will do all our work in.  We do this so
 	 * that we can reset the context during error recovery and thereby avoid
@@ -563,6 +573,19 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		/*
+		 * Since archive_cleanup_command can be changed at runtime, we have to
+		 * validate that only one of the library/command is set after every
+		 * SIGHUP.  Failing recovery seems harsh, so we just warn that the
+		 * shell command will be ignored.
+		 */
+		if (archiveCleanupCommand[0] != '\0' && archiveCleanupLibrary[0] != '\0')
+			ereport(WARNING,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("both archive_cleanup_command and archive_cleanup_library set"),
+					 errdetail("The value of archive_cleanup_command will be ignored."),
+					 errhint("Only one of archive_cleanup_command, archive_cleanup_library may be set.")));
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index f99186eab7..5dc830e6c0 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -133,7 +133,8 @@ 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 restore_command/library and
+ * recovery_end_command/library.
  */
 static void
 StartupRereadConfig(void)
@@ -161,6 +162,25 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	/*
+	 * Since restore_command and recovery_end_command can be changed at runtime,
+	 * we have to validate that only one of the library/command is set after
+	 * every SIGHUP.  Failing recovery seems harsh, so we just warn that the
+	 * shell command will be ignored.
+	 */
+	if (recoveryRestoreCommand[0] != '\0' && recoveryRestoreLibrary[0] != '\0')
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("both restore_command and restore_library set"),
+				 errdetail("The value of restore_command will be ignored."),
+				 errhint("Only one of restore_command, restore_library may be set.")));
+	if (recoveryEndCommand[0] != '\0' && recoveryEndLibrary[0] != '\0')
+		ereport(WARNING,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("both recovery_end_command and recovery_end_library set"),
+				 errdetail("The value of recovery_end_command will be ignored."),
+				 errhint("Only one of recovery_end_command, recovery_end_library may be set.")));
 }
 
 /* Handle various signals that might be sent to the startup process */
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index a37c9f9844..9aacb584f9 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -3764,6 +3764,16 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"restore_library", PGC_POSTMASTER, WAL_ARCHIVE_RECOVERY,
+			gettext_noop("Sets the library that will be called to retrieve an archived WAL file."),
+			gettext_noop("An empty string indicates that \"restore_command\" should be used.")
+		},
+		&recoveryRestoreLibrary,
+		"",
+		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."),
@@ -3774,6 +3784,16 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"archive_cleanup_library", PGC_POSTMASTER, WAL_ARCHIVE_RECOVERY,
+			gettext_noop("Sets the library that will be executed at every restart point."),
+			gettext_noop("An empty string indicates that \"archive_cleanup_command\" should be used.")
+		},
+		&archiveCleanupLibrary,
+		"",
+		NULL, NULL, NULL
+	},
+
 	{
 		{"recovery_end_command", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
 			gettext_noop("Sets the shell command that will be executed once at the end of recovery."),
@@ -3784,6 +3804,16 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"recovery_end_library", PGC_POSTMASTER, WAL_ARCHIVE_RECOVERY,
+			gettext_noop("Sets the library that will be executed once at the end of recovery."),
+			gettext_noop("An empty string indicates that \"recovery_end_command\" should be used.")
+		},
+		&recoveryEndLibrary,
+		"",
+		NULL, NULL, NULL
+	},
+
 	{
 		{"recovery_target_timeline", PGC_POSTMASTER, WAL_RECOVERY_TARGET,
 			gettext_noop("Specifies the timeline to recover into."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 5afdeb04de..13aa10b87e 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -269,8 +269,11 @@
 				# placeholders: %p = path of file to restore
 				#               %f = file name only
 				# e.g. 'cp /mnt/server/archivedir/%f %p'
+#restore_library = ''		# library to use to restore an archived WAL file
 #archive_cleanup_command = ''	# command to execute at every restartpoint
+#archive_cleanup_library = ''	# library to execute at every restartpoint
 #recovery_end_command = ''	# command to execute at completion of recovery
+#recovery_end_library = ''	# library to execute at completion of recovery
 
 # - Recovery Target -
 
diff --git a/src/include/access/xlog_internal.h b/src/include/access/xlog_internal.h
index e5fc66966b..467f95962b 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -400,5 +400,6 @@ extern PGDLLIMPORT bool ArchiveRecoveryRequested;
 extern PGDLLIMPORT bool InArchiveRecovery;
 extern PGDLLIMPORT bool StandbyMode;
 extern PGDLLIMPORT char *recoveryRestoreCommand;
+extern PGDLLIMPORT char *recoveryRestoreLibrary;
 
 #endif							/* XLOG_INTERNAL_H */
diff --git a/src/include/access/xlogarchive.h b/src/include/access/xlogarchive.h
index 69d002cdeb..c693d200c1 100644
--- a/src/include/access/xlogarchive.h
+++ b/src/include/access/xlogarchive.h
@@ -30,9 +30,44 @@ extern bool XLogArchiveIsReady(const char *xlog);
 extern bool XLogArchiveIsReadyOrDone(const char *xlog);
 extern void XLogArchiveCleanup(const char *xlog);
 
-extern bool shell_restore(const char *file, const char *path,
-						  const char *lastRestartPointFileName);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Recovery module callbacks
+ *
+ * These callback functions should be defined by recovery libraries and
+ * returned via _PG_recovery_module_init().  For more information about the
+ * purpose of each callback, refer to the recovery modules documentation.
+ */
+typedef bool (*RecoveryRestoreCB) (const char *file, const char *path,
+								   const char *lastRestartPointFileName);
+typedef void (*RecoveryArchiveCleanupCB) (const char *lastRestartPointFileName);
+typedef void (*RecoveryEndCB) (const char *lastRestartPointFileName);
+
+typedef struct RecoveryModuleCallbacks
+{
+	RecoveryRestoreCB restore_cb;
+	RecoveryArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndCB recovery_end_cb;
+} RecoveryModuleCallbacks;
+
+extern RecoveryModuleCallbacks RecoveryContext;
+
+/*
+ * Type of the shared library symbol _PG_recovery_module_init that is looked up
+ * when loading a recovery library.
+ */
+typedef void (*RecoveryModuleInit) (RecoveryModuleCallbacks *cb);
+
+extern PGDLLEXPORT void _PG_recovery_module_init(RecoveryModuleCallbacks *cb);
+
+extern void LoadRestoreLibrary(void);
+extern void LoadArchiveCleanupLibrary(void);
+extern void LoadRecoveryEndLibrary(void);
+
+/*
+ * Since the logic for recovery via a shell command is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern void shell_restore_init(RecoveryModuleCallbacks *cb);
 
 #endif							/* XLOG_ARCHIVE_H */
diff --git a/src/include/access/xlogrecovery.h b/src/include/access/xlogrecovery.h
index f3398425d8..f73fb12605 100644
--- a/src/include/access/xlogrecovery.h
+++ b/src/include/access/xlogrecovery.h
@@ -55,8 +55,11 @@ extern PGDLLIMPORT int recovery_min_apply_delay;
 extern PGDLLIMPORT char *PrimaryConnInfo;
 extern PGDLLIMPORT char *PrimarySlotName;
 extern PGDLLIMPORT char *recoveryRestoreCommand;
+extern PGDLLIMPORT char *recoveryRestoreLibrary;
 extern PGDLLIMPORT char *recoveryEndCommand;
+extern PGDLLIMPORT char *recoveryEndLibrary;
 extern PGDLLIMPORT char *archiveCleanupCommand;
+extern PGDLLIMPORT char *archiveCleanupLibrary;
 
 /* indirectly set via GUC system */
 extern PGDLLIMPORT TransactionId recoveryTargetXid;
-- 
2.25.1


--Qxx1br4bt0+wmkIi--





^ permalink  raw  reply  [nested|flat] 17+ messages in thread

* [PATCH v4 3/3] Allow recovery via loadable modules.
@ 2022-12-10 03:40 Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 17+ messages in thread

From: Nathan Bossart @ 2022-12-10 03:40 UTC (permalink / raw)

This adds the restore_library parameter to allow archive recovery
via a loadable module, rather than running shell commands.
---
 contrib/basic_archive/Makefile                |   4 +-
 contrib/basic_archive/basic_archive.c         |  67 ++++++-
 contrib/basic_archive/meson.build             |   7 +-
 contrib/basic_archive/t/001_restore.pl        |  44 +++++
 doc/src/sgml/archive-modules.sgml             | 168 ++++++++++++++++--
 doc/src/sgml/backup.sgml                      |  43 ++++-
 doc/src/sgml/basic-archive.sgml               |  33 ++--
 doc/src/sgml/config.sgml                      |  54 +++++-
 doc/src/sgml/high-availability.sgml           |  23 ++-
 src/backend/access/transam/shell_restore.c    |  21 ++-
 src/backend/access/transam/xlog.c             |  13 +-
 src/backend/access/transam/xlogarchive.c      |  70 +++++++-
 src/backend/access/transam/xlogrecovery.c     |  26 ++-
 src/backend/postmaster/checkpointer.c         |  26 +++
 src/backend/postmaster/pgarch.c               |   7 +-
 src/backend/postmaster/startup.c              |  23 ++-
 src/backend/utils/misc/guc.c                  |  14 ++
 src/backend/utils/misc/guc_tables.c           |  10 ++
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/access/xlog_internal.h            |   1 +
 src/include/access/xlogarchive.h              |  44 ++++-
 src/include/access/xlogrecovery.h             |   1 +
 src/include/utils/guc.h                       |   2 +
 23 files changed, 618 insertions(+), 84 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 55d299d650..487dc563f3 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -1,7 +1,7 @@
 # contrib/basic_archive/Makefile
 
 MODULES = basic_archive
-PGFILEDESC = "basic_archive - basic archive module"
+PGFILEDESC = "basic_archive - basic archive and recovery module"
 
 REGRESS = basic_archive
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.conf
@@ -9,6 +9,8 @@ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.c
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
 
+TAP_TESTS = 1
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index 28cbb6cce0..8c333c8f99 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
@@ -30,6 +35,7 @@
 #include <sys/time.h>
 #include <unistd.h>
 
+#include "access/xlogarchive.h"
 #include "common/int.h"
 #include "miscadmin.h"
 #include "postmaster/pgarch.h"
@@ -48,6 +54,8 @@ static bool basic_archive_file(const char *file, const char *path);
 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 bool basic_restore_file(const char *file, const char *path,
+							   const char *lastRestartPointFileName);
 
 /*
  * _PG_init
@@ -87,6 +95,19 @@ _PG_archive_module_init(ArchiveModuleCallbacks *cb)
 	cb->archive_file_cb = basic_archive_file;
 }
 
+/*
+ * _PG_recovery_module_init
+ *
+ * Returns the module's restore callback.
+ */
+void
+_PG_recovery_module_init(RecoveryModuleCallbacks *cb)
+{
+	AssertVariableIsOfType(&_PG_recovery_module_init, RecoveryModuleInit);
+
+	cb->restore_cb = basic_restore_file;
+}
+
 /*
  * check_archive_directory
  *
@@ -99,8 +120,8 @@ check_archive_directory(char **newval, void **extra, GucSource source)
 
 	/*
 	 * The default value is an empty string, so we have to accept that value.
-	 * Our check_configured callback also checks for this and prevents
-	 * archiving from proceeding if it is still empty.
+	 * Our check_configured and restore callbacks also check for this and
+	 * prevent archiving or recovery from proceeding if it is still empty.
 	 */
 	if (*newval == NULL || *newval[0] == '\0')
 		return true;
@@ -368,3 +389,45 @@ compare_files(const char *file1, const char *file2)
 
 	return ret;
 }
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file from the WAL archives.
+ */
+static bool
+basic_restore_file(const char *file, const char *path,
+				   const char *lastRestartPointFileName)
+{
+	char		source[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive", file)));
+
+	if (archive_directory == NULL || archive_directory[0] == '\0')
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"basic_archive.archive_directory\" is not set")));
+
+	/*
+	 * Check whether the file exists.  If not, we return false to indicate that
+	 * there are no more files to restore.
+	 */
+	snprintf(source, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(source, &st) != 0)
+	{
+		int		elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m", source)));
+		return false;
+	}
+
+	copy_file(source, unconstify(char *, path));
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive", file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index bc1380e6f6..af4580dea9 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -7,7 +7,7 @@ basic_archive_sources = files(
 if host_system == 'windows'
   basic_archive_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
     '--NAME', 'basic_archive',
-    '--FILEDESC', 'basic_archive - basic archive module',])
+    '--FILEDESC', 'basic_archive - basic archive and recovery module',])
 endif
 
 basic_archive = shared_module('basic_archive',
@@ -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..ec8767d740
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,44 @@
+
+# Copyright (c) 2022, 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-modules.sgml b/doc/src/sgml/archive-modules.sgml
index ef02051f7f..53e657040b 100644
--- a/doc/src/sgml/archive-modules.sgml
+++ b/doc/src/sgml/archive-modules.sgml
@@ -1,34 +1,40 @@
 <!-- doc/src/sgml/archive-modules.sgml -->
 
 <chapter id="archive-modules">
- <title>Archive Modules</title>
+ <title>Archive and Recovery Modules</title>
  <indexterm zone="archive-modules">
-  <primary>Archive Modules</primary>
+  <primary>Archive and Recovery Modules</primary>
  </indexterm>
 
  <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 (e.g., <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>
   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"/>.
+  were successfully archived.  When a custom
+  <xref linkend="guc-restore-library"/> is configured, PostgreSQL will use the
+  module for recovery actions.  It is ultimately up to the module to decide how
+  to accomplish each task, but some recommendations are listed at
+  <xref linkend="backup-archiving-wal"/> and
+  <xref linkend="backup-pitr-recovery"/>.
  </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).
+  Archive and recovery modules must at least consist of an initialization
+  function (see <xref linkend="archive-module-init"/> and
+  <xref linkend="recovery-module-init"/>) and the required callbacks (see
+  <xref linkend="archive-module-callbacks"/> and
+  <xref linkend="recovery-module-callbacks"/>).  However, archive and recovery
+  modules are also permitted to do much more (e.g., declare GUCs and register
+  background workers).  A module may be used for both
+  <varname>archive_library</varname> and <varname>restore_library</varname>.
  </para>
 
  <para>
@@ -37,7 +43,7 @@
  </para>
 
  <sect1 id="archive-module-init">
-  <title>Initialization Functions</title>
+  <title>Archive Module Initialization Functions</title>
   <indexterm zone="archive-module-init">
    <primary>_PG_archive_module_init</primary>
   </indexterm>
@@ -64,6 +70,12 @@ typedef void (*ArchiveModuleInit) (struct ArchiveModuleCallbacks *cb);
    Only the <function>archive_file_cb</function> callback is required.  The
    others are optional.
   </para>
+
+  <note>
+   <para>
+    <varname>archive_library</varname> is only loaded in the archiver process.
+   </para>
+  </note>
  </sect1>
 
  <sect1 id="archive-module-callbacks">
@@ -129,6 +141,132 @@ typedef bool (*ArchiveFileCB) (const char *file, const char *path);
 
 <programlisting>
 typedef void (*ArchiveShutdownCB) (void);
+</programlisting>
+   </para>
+  </sect2>
+ </sect1>
+
+ <sect1 id="recovery-module-init">
+  <title>Recovery Module Initialization Functions</title>
+  <indexterm zone="recovery-module-init">
+   <primary>_PG_recovery_module_init</primary>
+  </indexterm>
+  <para>
+   A recovery library is loaded by dynamically loading a shared library with the
+   <xref linkend="guc-restore-library"/> as the library base name.  The normal
+   library search path is used to locate the library.  To provide the required
+   recovery module callbacks and to indicate that the library is actually a
+   recovery module, it needs to provide a function named
+   <function>_PG_recovery_module_init</function>.  This function is passed a
+   struct that needs to be filled with the callback function pointers for
+   individual actions.
+
+<programlisting>
+typedef struct RecoveryModuleCallbacks
+{
+    RecoveryRestoreCB restore_cb;
+    RecoveryArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndCB recovery_end_cb;
+    RecoveryShutdownCB shutdown_cb;
+} RecoveryModuleCallbacks;
+typedef void (*RecoveryModuleInit) (struct RecoveryModuleCallbacks *cb);
+</programlisting>
+
+   The <function>restore_cb</function> callback is required for archive
+   recovery, but it is optional for streaming replication.  The others are
+   always optional.
+  </para>
+
+  <note>
+   <para>
+    <varname>restore_library</varname> is only loaded in the startup and
+    checkpointer processes and in single-user mode.
+   </para>
+  </note>
+ </sect1>
+
+ <sect1 id="recovery-module-callbacks">
+  <title>Recovery Module Callbacks</title>
+  <para>
+   The recovery callbacks define the actual behavior of the module.  The server
+   will call them as required to execute recovery actions.
+  </para>
+
+  <sect2 id="recovery-module-restore">
+   <title>Restore Callback</title>
+   <para>
+    The <function>restore_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 (*RecoveryRestoreCB) (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>
+  </sect2>
+
+  <sect2 id="recovery-module-archive-cleanup">
+   <title>Archive Cleanup Callback</title>
+   <para>
+    The <function>archive_cleanup_cb</function> callback is called at every
+    restart point 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 (*RecoveryArchiveCleanupCB) (const char *lastRestartPointFileName);
+</programlisting>
+
+    <replaceable>lastRestartPointFileName</replaceable> will contain the name
+    of the file containing the last valid restart point, like in
+    <link linkend="recovery-module-restore"><function>restore_cb</function></link>.
+   </para>
+  </sect2>
+
+  <sect2 id="recovery-module-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
+    replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (const char *lastRestartPointFileName);
+</programlisting>
+
+    <replaceable>lastRestartPointFileName</replaceable> will contain the name
+    of the file containing the last valid restart point, like in
+    <link linkend="recovery-module-restore"><function>restore_cb</function></link>.
+   </para>
+  </sect2>
+
+  <sect2 id="recovery-module-shutdown">
+   <title>Shutdown Callback</title>
+   <para>
+    The <function>shutdown_cb</function> callback is called when a process that
+    has loaded the recovery 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.
+
+<programlisting>
+typedef void (*RecoveryShutdownCB) (void);
 </programlisting>
    </para>
   </sect2>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index 8bab521718..d6d34078fc 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1181,9 +1181,27 @@ 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>,
-    which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>
+    that defines a restore callback, which tells
+    <productname>PostgreSQL</productname> how to retrieve archived 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,
+    recovery modules can be more performant 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="archive-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.
@@ -1202,14 +1220,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 function returns
+    <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 function provided by the
+    <varname>restore_library</varname> emits an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1233,7 +1257,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 0b650f17a8..ac7cd9b967 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 recovery
+  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 recovery modules.  For
+  more information about archive and recovery modules, see
+  see <xref linkend="archive-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 recovery 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>
@@ -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,7 +65,8 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
-   Server crashes may leave temporary files with the prefix
+   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
    remove such files while the server is running as long as they are unrelated
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 05b3862d09..899d2b5fdb 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3773,7 +3773,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
@@ -3801,7 +3802,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,
@@ -3836,7 +3838,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 recovery actions, including retrieving archived
+        segments of the WAL file series 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 recovery.
+        For more information, see <xref linkend="archive-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>
@@ -3881,7 +3918,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>
@@ -3910,11 +3950,13 @@ 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>
-
     </variablelist>
 
   </sect2>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index f180607528..6266e2df7f 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,9 +639,11 @@ 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
-    reaches the end of WAL available there and <varname>restore_command</varname>
-    fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s restore callback.
+    Once it reaches the end of WAL available there and
+    <varname>restore_command</varname> or the restore callback 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
     from the last valid record found in archive or <filename>pg_wal</filename>. If that fails
@@ -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 restore callbacks provided by
+     <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,10 @@ 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
+    <function>archive_cleanup_cb</function> callback function 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/shell_restore.c b/src/backend/access/transam/shell_restore.c
index 073e709e06..124a0bbdb6 100644
--- a/src/backend/access/transam/shell_restore.c
+++ b/src/backend/access/transam/shell_restore.c
@@ -3,7 +3,8 @@
  * shell_restore.c
  *
  * These recovery 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 recovery logic.
  *
  * Copyright (c) 2022, PostgreSQL Global Development Group
  *
@@ -22,6 +23,10 @@
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static bool shell_restore(const char *file, const char *path,
+						  const char *lastRestartPointFileName);
+static void shell_archive_cleanup(const char *lastRestartPointFileName);
+static void shell_recovery_end(const char *lastRestartPointFileName);
 static char *BuildCleanupCommand(const char *command,
 								 const char *lastRestartPointFileName);
 static bool ExecuteRecoveryCommand(const char *command,
@@ -29,6 +34,16 @@ static bool ExecuteRecoveryCommand(const char *command,
 								   bool exitOnSigterm, uint32 wait_event_info,
 								   int fail_elevel);
 
+void
+shell_restore_init(RecoveryModuleCallbacks *cb)
+{
+	AssertVariableIsOfType(&shell_restore_init, RecoveryModuleInit);
+
+	cb->restore_cb = shell_restore;
+	cb->archive_cleanup_cb = shell_archive_cleanup;
+	cb->recovery_end_cb = shell_recovery_end;
+}
+
 bool
 shell_restore(const char *file, const char *path,
 			  const char *lastRestartPointFileName)
@@ -73,7 +88,7 @@ shell_restore(const char *file, const char *path,
 	return ret;
 }
 
-void
+static void
 shell_archive_cleanup(const char *lastRestartPointFileName)
 {
 	char	   *cmd = BuildCleanupCommand(archiveCleanupCommand,
@@ -84,7 +99,7 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 	pfree(cmd);
 }
 
-void
+static void
 shell_recovery_end(const char *lastRestartPointFileName)
 {
 	char	   *cmd = BuildCleanupCommand(recoveryEndCommand,
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index fdce12614a..c4ad571327 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4883,10 +4883,11 @@ static void
 CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
+
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 */
-	if (recoveryEndCommand && strcmp(recoveryEndCommand, "") != 0)
+	if (RecoveryContext.recovery_end_cb)
 	{
 		char		lastRestartPointFname[MAXPGPATH];
 		XLogSegNo	restartSegNo;
@@ -4903,7 +4904,7 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 		XLogFileName(lastRestartPointFname, restartTli, restartSegNo,
 					 wal_segment_size);
 
-		shell_recovery_end(lastRestartPointFname);
+		RecoveryContext.recovery_end_cb(lastRestartPointFname);
 	}
 
 	/*
@@ -7318,9 +7319,9 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Execute the archive-cleanup callback, if any.
 	 */
-	if (archiveCleanupCommand && strcmp(archiveCleanupCommand, "") != 0)
+	if (RecoveryContext.archive_cleanup_cb)
 	{
 		char		lastRestartPointFname[MAXPGPATH];
 		XLogSegNo	restartSegNo;
@@ -7337,7 +7338,7 @@ CreateRestartPoint(int flags)
 		XLogFileName(lastRestartPointFname, restartTli, restartSegNo,
 					 wal_segment_size);
 
-		shell_archive_cleanup(lastRestartPointFname);
+		RecoveryContext.archive_cleanup_cb(lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index b5cb060d55..fdab7dad43 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -22,7 +22,9 @@
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
+#include "access/xlogrecovery.h"
 #include "common/archive.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/startup.h"
@@ -32,6 +34,11 @@
 #include "storage/ipc.h"
 #include "storage/lwlock.h"
 
+/*
+ * Global context for recovery-related callbacks.
+ */
+RecoveryModuleCallbacks RecoveryContext;
+
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
  * If successful, fill "path" with its complete path (note that this will be
@@ -71,7 +78,7 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 		goto not_available;
 
 	/* In standby mode, restore_command might not be supplied */
-	if (recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0)
+	if (RecoveryContext.restore_cb == NULL)
 		goto not_available;
 
 	/*
@@ -149,14 +156,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 		XLogFileName(lastRestartPointFname, 0, 0L, wal_segment_size);
 
 	/*
-	 * Check signals before restore command and reset afterwards.
+	 * Check signals before restore callback and reset afterwards.
 	 */
 	PreRestoreCommand();
 
 	/*
 	 * Copy xlog from archival storage to XLOGDIR
 	 */
-	ret = shell_restore(xlogfname, xlogpath, lastRestartPointFname);
+	ret = RecoveryContext.restore_cb(xlogfname, xlogpath,
+									 lastRestartPointFname);
 
 	PostRestoreCommand();
 
@@ -603,3 +611,59 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the recovery callbacks into our global RecoveryContext.  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
+LoadRecoveryCallbacks(void)
+{
+	RecoveryModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_recovery_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RecoveryModuleInit)
+			load_external_function(restoreLibrary, "_PG_recovery_module_init",
+								   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("recovery modules have to define the symbol "
+						"_PG_recovery_module_init")));
+
+	memset(&RecoveryContext, 0, sizeof(RecoveryModuleCallbacks));
+	(*init) (&RecoveryContext);
+
+	/*
+	 * If using shell commands, remove callbacks for any commands that are not
+	 * set.
+	 */
+	if (restoreLibrary[0] == '\0')
+	{
+		if (recoveryRestoreCommand[0] == '\0')
+			RecoveryContext.restore_cb = NULL;
+		if (archiveCleanupCommand[0] == '\0')
+			RecoveryContext.archive_cleanup_cb = NULL;
+		if (recoveryEndCommand[0] == '\0')
+			RecoveryContext.recovery_end_cb = NULL;
+	}
+}
+
+/*
+ * Call the shutdown callback of the loaded recovery module, if defined.
+ */
+void
+call_recovery_module_shutdown_cb(int code, Datum arg)
+{
+	if (RecoveryContext.shutdown_cb)
+		RecoveryContext.shutdown_cb();
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index bc3c3eb3e7..95bcc3a0b7 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -80,6 +80,7 @@ const struct config_enum_entry recovery_target_action_options[] = {
 
 /* options formerly taken from recovery.conf for archive recovery */
 char	   *recoveryRestoreCommand = NULL;
+char	   *restoreLibrary = NULL;
 char	   *recoveryEndCommand = NULL;
 char	   *archiveCleanupCommand = NULL;
 RecoveryTargetType recoveryTarget = RECOVERY_TARGET_UNSET;
@@ -1053,24 +1054,37 @@ validateRecoveryParameters(void)
 	if (!ArchiveRecoveryRequested)
 		return;
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryRestoreCommand, "restore_command");
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryEndCommand, "recovery_end_command");
+	before_shmem_exit(call_recovery_module_shutdown_cb, 0);
+	LoadRecoveryCallbacks();
+
 	/*
 	 * Check for compulsory parameters
 	 */
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			RecoveryContext.restore_cb == NULL)
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
-					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
+					(errmsg("specified neither primary_conninfo nor restore_command "
+							"nor a restore_library that defines a restore callback"),
+					 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 (RecoveryContext.restore_cb == NULL)
 			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 restore_library that defines "
+							"a restore callback when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index de0bbbfa79..6350fd0b83 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -38,6 +38,7 @@
 
 #include "access/xlog.h"
 #include "access/xlog_internal.h"
+#include "access/xlogarchive.h"
 #include "access/xlogrecovery.h"
 #include "libpq/pqsignal.h"
 #include "miscadmin.h"
@@ -222,6 +223,16 @@ CheckpointerMain(void)
 	 */
 	before_shmem_exit(pgstat_before_server_shutdown, 0);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.  We do this before setting up the exception handler
+	 * so that any problems result in a server crash shortly after startup.
+	 */
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   archiveCleanupCommand, "archive_cleanup_command");
+	before_shmem_exit(call_recovery_module_shutdown_cb, 0);
+	LoadRecoveryCallbacks();
+
 	/*
 	 * Create a memory context that we will do all our work in.  We do this so
 	 * that we can reset the context during error recovery and thereby avoid
@@ -548,6 +559,9 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -563,6 +577,18 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+								   archiveCleanupCommand, "archive_cleanup_command");
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			call_recovery_module_shutdown_cb(0, (Datum) 0);
+			LoadRecoveryCallbacks();
+		}
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c
index 8ecdb9ca23..8e91f2d70f 100644
--- a/src/backend/postmaster/pgarch.c
+++ b/src/backend/postmaster/pgarch.c
@@ -831,11 +831,8 @@ 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.")));
+	CheckMutuallyExclusiveGUCs(XLogArchiveLibrary, "archive_library",
+							   XLogArchiveCommand, "archive_command");
 
 	memset(&ArchiveContext, 0, sizeof(ArchiveModuleCallbacks));
 
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index 8786186898..f9ff2b5583 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -20,6 +20,7 @@
 #include "postgres.h"
 
 #include "access/xlog.h"
+#include "access/xlogarchive.h"
 #include "access/xlogrecovery.h"
 #include "access/xlogutils.h"
 #include "libpq/pqsignal.h"
@@ -133,13 +134,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 recovery 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;
@@ -161,6 +166,22 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryRestoreCommand, "restore_command");
+	CheckMutuallyExclusiveGUCs(restoreLibrary, "restore_library",
+							   recoveryEndCommand, "recovery_end_command");
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_recovery_module_shutdown_cb(0, (Datum) 0);
+		LoadRecoveryCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index d52069f446..7858e9a649 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -6880,3 +6880,17 @@ call_enum_check_hook(struct config_enum *conf, int *newval, void **extra,
 
 	return true;
 }
+
+/*
+ * ERROR if both parameters are set.
+ */
+void
+CheckMutuallyExclusiveGUCs(const char *p1val, const char *p1name,
+						   const char *p2val, const char *p2name)
+{
+	if (p1val[0] != '\0' && p2val[0] != '\0')
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("both %s and %s set", p1name, p2name),
+				 errdetail("Only one of %s, %s may be set.", p1name, p2name)));
+}
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 68328b1402..19746e2489 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -3764,6 +3764,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 recovery 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 5afdeb04de..e71e79271a 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -269,6 +269,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 recovery 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/access/xlog_internal.h b/src/include/access/xlog_internal.h
index 59fc7bc105..756f0898b5 100644
--- a/src/include/access/xlog_internal.h
+++ b/src/include/access/xlog_internal.h
@@ -400,5 +400,6 @@ extern PGDLLIMPORT bool ArchiveRecoveryRequested;
 extern PGDLLIMPORT bool InArchiveRecovery;
 extern PGDLLIMPORT bool StandbyMode;
 extern PGDLLIMPORT char *recoveryRestoreCommand;
+extern PGDLLIMPORT char *restoreLibrary;
 
 #endif							/* XLOG_INTERNAL_H */
diff --git a/src/include/access/xlogarchive.h b/src/include/access/xlogarchive.h
index 299304703e..71c9b88165 100644
--- a/src/include/access/xlogarchive.h
+++ b/src/include/access/xlogarchive.h
@@ -30,9 +30,45 @@ extern bool XLogArchiveIsReady(const char *xlog);
 extern bool XLogArchiveIsReadyOrDone(const char *xlog);
 extern void XLogArchiveCleanup(const char *xlog);
 
-extern bool shell_restore(const char *file, const char *path,
-						  const char *lastRestartPointFileName);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Recovery module callbacks
+ *
+ * These callback functions should be defined by recovery libraries and
+ * returned via _PG_recovery_module_init().  For more information about the
+ * purpose of each callback, refer to the recovery modules documentation.
+ */
+typedef bool (*RecoveryRestoreCB) (const char *file, const char *path,
+								   const char *lastRestartPointFileName);
+typedef void (*RecoveryArchiveCleanupCB) (const char *lastRestartPointFileName);
+typedef void (*RecoveryEndCB) (const char *lastRestartPointFileName);
+typedef void (*RecoveryShutdownCB) (void);
+
+typedef struct RecoveryModuleCallbacks
+{
+	RecoveryRestoreCB restore_cb;
+	RecoveryArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndCB recovery_end_cb;
+	RecoveryShutdownCB shutdown_cb;
+} RecoveryModuleCallbacks;
+
+extern RecoveryModuleCallbacks RecoveryContext;
+
+/*
+ * Type of the shared library symbol _PG_recovery_module_init that is looked up
+ * when loading a recovery library.
+ */
+typedef void (*RecoveryModuleInit) (RecoveryModuleCallbacks *cb);
+
+extern PGDLLEXPORT void _PG_recovery_module_init(RecoveryModuleCallbacks *cb);
+
+extern void LoadRecoveryCallbacks(void);
+extern void call_recovery_module_shutdown_cb(int code, Datum arg);
+
+/*
+ * Since the logic for recovery via a shell command is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern void shell_restore_init(RecoveryModuleCallbacks *cb);
 
 #endif							/* XLOG_ARCHIVE_H */
diff --git a/src/include/access/xlogrecovery.h b/src/include/access/xlogrecovery.h
index 47c29350f5..35d1d09374 100644
--- a/src/include/access/xlogrecovery.h
+++ b/src/include/access/xlogrecovery.h
@@ -55,6 +55,7 @@ extern PGDLLIMPORT int recovery_min_apply_delay;
 extern PGDLLIMPORT char *PrimaryConnInfo;
 extern PGDLLIMPORT char *PrimarySlotName;
 extern PGDLLIMPORT char *recoveryRestoreCommand;
+extern PGDLLIMPORT char *restoreLibrary;
 extern PGDLLIMPORT char *recoveryEndCommand;
 extern PGDLLIMPORT char *archiveCleanupCommand;
 
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index ba89d013e6..947597247f 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -404,6 +404,8 @@ extern void *guc_malloc(int elevel, size_t size);
 extern pg_nodiscard void *guc_realloc(int elevel, void *old, size_t size);
 extern char *guc_strdup(int elevel, const char *src);
 extern void guc_free(void *ptr);
+extern void CheckMutuallyExclusiveGUCs(const char *p1val, const char *p1name,
+									   const char *p2val, const char *p2name);
 
 #ifdef EXEC_BACKEND
 extern void write_nondefault_variables(GucContext context);
-- 
2.25.1


--Kj7319i9nmIyA2yE--





^ permalink  raw  reply  [nested|flat] 17+ messages in thread

* Re: Check lateral references within PHVs for memoize cache keys
@ 2023-12-25 07:01 Richard Guo <[email protected]>
  2024-02-02 09:18 ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  0 siblings, 1 reply; 17+ messages in thread

From: Richard Guo @ 2023-12-25 07:01 UTC (permalink / raw)
  To: Tom Lane <[email protected]>; +Cc: pgsql-hackers; David Rowley <[email protected]>

On Thu, Jul 13, 2023 at 3:12 PM Richard Guo <[email protected]> wrote:

> So I'm wondering if it'd be better that we move all this logic of
> computing additional lateral references within PHVs to get_memoize_path,
> where we can examine only PHVs that are evaluated at innerrel.  And
> considering that these lateral refs are only used by Memoize, it seems
> more sensible to compute them there.  But I'm a little worried that
> doing this would make get_memoize_path too expensive.
>
> Please see v4 patch for this change.
>

I'd like to add that not checking PHVs for lateral references can lead
to performance regressions with Memoize node.  For instance,

-- by default, enable_memoize is on
regression=# explain (analyze, costs off) select * from tenk1 t1 left join
lateral (select *, t1.four as x from tenk1 t2) s on t1.two = s.two;
                                     QUERY PLAN
-------------------------------------------------------------------------------------
 Nested Loop Left Join (actual time=0.028..105245.547 rows=50000000 loops=1)
   ->  Seq Scan on tenk1 t1 (actual time=0.011..3.760 rows=10000 loops=1)
   ->  Memoize (actual time=0.010..8.051 rows=5000 loops=10000)
         Cache Key: t1.two
         Cache Mode: logical
         Hits: 0  Misses: 10000  Evictions: 9999  Overflows: 0  Memory
Usage: 1368kB
         ->  Seq Scan on tenk1 t2 (actual time=0.004..3.594 rows=5000
loops=10000)
               Filter: (t1.two = two)
               Rows Removed by Filter: 5000
 Planning Time: 1.943 ms
 Execution Time: 106806.043 ms
(11 rows)

-- turn enable_memoize off
regression=# set enable_memoize to off;
SET
regression=# explain (analyze, costs off) select * from tenk1 t1 left join
lateral (select *, t1.four as x from tenk1 t2) s on t1.two = s.two;
                                 QUERY PLAN
-----------------------------------------------------------------------------
 Nested Loop Left Join (actual time=0.048..44831.707 rows=50000000 loops=1)
   ->  Seq Scan on tenk1 t1 (actual time=0.026..2.340 rows=10000 loops=1)
   ->  Seq Scan on tenk1 t2 (actual time=0.002..3.282 rows=5000 loops=10000)
         Filter: (t1.two = two)
         Rows Removed by Filter: 5000
 Planning Time: 0.641 ms
 Execution Time: 46472.609 ms
(7 rows)

As we can see, when Memoize enabled (which is the default setting), the
execution time increases by around 129.83%, indicating a significant
performance regression.

This is caused by that we fail to realize that 't1.four', which is from
the PHV, should be included in the cache keys.  And that makes us have
to purge the entire cache every time we get a new outer tuple.  This is
also implied by the abnormal Memoize runtime stats:

    Hits: 0  Misses: 10000  Evictions: 9999  Overflows: 0

This regression can be fixed by the patch here.  After applying the v4
patch, 't1.four' is added into the cache keys, and the same query runs
much faster.

regression=# explain (analyze, costs off) select * from tenk1 t1 left join
lateral (select *, t1.four as x from tenk1 t2) s on t1.two = s.two;
                                   QUERY PLAN
---------------------------------------------------------------------------------
 Nested Loop Left Join (actual time=0.060..20446.004 rows=50000000 loops=1)
   ->  Seq Scan on tenk1 t1 (actual time=0.027..5.845 rows=10000 loops=1)
   ->  Memoize (actual time=0.001..0.209 rows=5000 loops=10000)
         Cache Key: t1.two, t1.four
         Cache Mode: binary
         Hits: 9996  Misses: 4  Evictions: 0  Overflows: 0  Memory Usage:
5470kB
         ->  Seq Scan on tenk1 t2 (actual time=0.005..3.659 rows=5000
loops=4)
               Filter: (t1.two = two)
               Rows Removed by Filter: 5000
 Planning Time: 0.579 ms
 Execution Time: 21756.598 ms
(11 rows)

Comparing the first plan and the third plan, this query runs ~5 times
faster.

Thanks
Richard


^ permalink  raw  reply  [nested|flat] 17+ messages in thread

* Re: Check lateral references within PHVs for memoize cache keys
  2023-12-25 07:01 Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
@ 2024-02-02 09:18 ` Richard Guo <[email protected]>
  2024-03-18 08:36   ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  0 siblings, 1 reply; 17+ messages in thread

From: Richard Guo @ 2024-02-02 09:18 UTC (permalink / raw)
  To: Tom Lane <[email protected]>; +Cc: pgsql-hackers; David Rowley <[email protected]>

On Mon, Dec 25, 2023 at 3:01 PM Richard Guo <[email protected]> wrote:

> On Thu, Jul 13, 2023 at 3:12 PM Richard Guo <[email protected]>
> wrote:
>
>> So I'm wondering if it'd be better that we move all this logic of
>> computing additional lateral references within PHVs to get_memoize_path,
>> where we can examine only PHVs that are evaluated at innerrel.  And
>> considering that these lateral refs are only used by Memoize, it seems
>> more sensible to compute them there.  But I'm a little worried that
>> doing this would make get_memoize_path too expensive.
>>
>> Please see v4 patch for this change.
>>
>
> I'd like to add that not checking PHVs for lateral references can lead
> to performance regressions with Memoize node.
>

The v4 patch does not apply any more.  I've rebased it on master.
Nothing else has changed.

Thanks
Richard


Attachments:

  [application/octet-stream] v5-0001-Check-lateral-refs-within-PHVs-for-memoize-cache-keys.patch (11.2K, ../../CAMbWs49Z6TrH4qJuudPx2toeJutPOV1CyvbL3GE1QNg_LaATEg@mail.gmail.com/3-v5-0001-Check-lateral-refs-within-PHVs-for-memoize-cache-keys.patch)
  download | inline diff:
From da5be2463902c1b97e5d6299f61fc71d0b90126b Mon Sep 17 00:00:00 2001
From: Richard Guo <[email protected]>
Date: Fri, 30 Dec 2022 10:35:36 +0800
Subject: [PATCH v5] Check lateral refs within PHVs for memoize cache keys

---
 .../postgres_fdw/expected/postgres_fdw.out    | 12 ++-
 src/backend/optimizer/path/joinpath.c         | 85 ++++++++++++++++++-
 src/test/regress/expected/memoize.out         | 68 +++++++++++++++
 src/test/regress/sql/memoize.sql              | 24 ++++++
 4 files changed, 181 insertions(+), 8 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index b5a38aeb21..c25fd9d076 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -3696,15 +3696,19 @@ ORDER BY ref_0."C 1";
          ->  Index Scan using t1_pkey on "S 1"."T 1" ref_0
                Output: ref_0."C 1", ref_0.c2, ref_0.c3, ref_0.c4, ref_0.c5, ref_0.c6, ref_0.c7, ref_0.c8
                Index Cond: (ref_0."C 1" < 10)
-         ->  Foreign Scan on public.ft1 ref_1
-               Output: ref_1.c3, ref_0.c2
-               Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE ((c3 = '00001'))
+         ->  Memoize
+               Output: ref_1.c3, (ref_0.c2)
+               Cache Key: ref_0.c2
+               Cache Mode: binary
+               ->  Foreign Scan on public.ft1 ref_1
+                     Output: ref_1.c3, ref_0.c2
+                     Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE ((c3 = '00001'))
    ->  Materialize
          Output: ref_3.c3
          ->  Foreign Scan on public.ft2 ref_3
                Output: ref_3.c3
                Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE ((c3 = '00001'))
-(15 rows)
+(19 rows)
 
 SELECT ref_0.c2, subq_1.*
 FROM
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 6aca66f196..e4dae8e534 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -23,6 +23,7 @@
 #include "optimizer/optimizer.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
+#include "optimizer/placeholder.h"
 #include "optimizer/planmain.h"
 #include "utils/typcache.h"
 
@@ -437,10 +438,11 @@ have_unsafe_outer_join_ref(PlannerInfo *root,
 static bool
 paraminfo_get_equal_hashops(PlannerInfo *root, ParamPathInfo *param_info,
 							RelOptInfo *outerrel, RelOptInfo *innerrel,
-							List **param_exprs, List **operators,
-							bool *binary_mode)
+							List *ph_lateral_vars, List **param_exprs,
+							List **operators, bool *binary_mode)
 
 {
+	List	   *lateral_vars;
 	ListCell   *lc;
 
 	*param_exprs = NIL;
@@ -520,7 +522,8 @@ paraminfo_get_equal_hashops(PlannerInfo *root, ParamPathInfo *param_info,
 	}
 
 	/* Now add any lateral vars to the cache key too */
-	foreach(lc, innerrel->lateral_vars)
+	lateral_vars = list_concat(ph_lateral_vars, innerrel->lateral_vars);
+	foreach(lc, lateral_vars)
 	{
 		Node	   *expr = (Node *) lfirst(lc);
 		TypeCacheEntry *typentry;
@@ -571,6 +574,71 @@ paraminfo_get_equal_hashops(PlannerInfo *root, ParamPathInfo *param_info,
 	return true;
 }
 
+/*
+ * extract_lateral_vars_from_PHVs
+ *	  Extract lateral references within PlaceHolderVars that are due to be
+ *	  evaluated at 'innerrelids'.
+ */
+static List *
+extract_lateral_vars_from_PHVs(PlannerInfo *root, Relids innerrelids)
+{
+	List	   *ph_lateral_vars = NIL;
+	ListCell   *lc;
+
+	/* Nothing would be found if the query contains no LATERAL RTEs */
+	if (!root->hasLateralRTEs)
+		return NIL;
+
+	foreach(lc, root->placeholder_list)
+	{
+		PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(lc);
+		List	   *vars;
+		ListCell   *cell;
+
+		/* PHV is uninteresting if no lateral refs */
+		if (phinfo->ph_lateral == NULL)
+			continue;
+
+		/* PHV is uninteresting if not due to be evaluated at innerrelids */
+		if (!bms_equal(phinfo->ph_eval_at, innerrelids))
+			continue;
+
+		/* Fetch Vars and PHVs of lateral references within PlaceHolderVars */
+		vars = pull_vars_of_level((Node *) phinfo->ph_var->phexpr, 0);
+		foreach(cell, vars)
+		{
+			Node	   *node = (Node *) lfirst(cell);
+
+			node = copyObject(node);
+			if (IsA(node, Var))
+			{
+				Var		   *var = (Var *) node;
+
+				Assert(var->varlevelsup == 0);
+
+				if (bms_is_member(var->varno, phinfo->ph_lateral))
+					ph_lateral_vars = lappend(ph_lateral_vars, node);
+			}
+			else if (IsA(node, PlaceHolderVar))
+			{
+				PlaceHolderVar *phv = (PlaceHolderVar *) node;
+
+				Assert(phv->phlevelsup == 0);
+
+				if (bms_is_subset(find_placeholder_info(root, phv)->ph_eval_at,
+								  phinfo->ph_lateral))
+					ph_lateral_vars = lappend(ph_lateral_vars, node);
+			}
+			else
+				Assert(false);
+		}
+
+		list_free(vars);
+	}
+
+	return ph_lateral_vars;
+}
+
 /*
  * get_memoize_path
  *		If possible, make and return a Memoize path atop of 'inner_path'.
@@ -586,6 +654,7 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel,
 	List	   *hash_operators;
 	ListCell   *lc;
 	bool		binary_mode;
+	List	   *ph_lateral_vars;
 
 	/* Obviously not if it's disabled */
 	if (!enable_memoize)
@@ -600,6 +669,12 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel,
 	if (outer_path->parent->rows < 2)
 		return NULL;
 
+	/*
+	 * Extract lateral Vars within PlaceHolderVars that are due to be evaluated
+	 * at innerrel.  These lateral Vars could be used as memoize cache keys.
+	 */
+	ph_lateral_vars = extract_lateral_vars_from_PHVs(root, innerrel->relids);
+
 	/*
 	 * We can only have a memoize node when there's some kind of cache key,
 	 * either parameterized path clauses or lateral Vars.  No cache key sounds
@@ -607,7 +682,8 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel,
 	 */
 	if ((inner_path->param_info == NULL ||
 		 inner_path->param_info->ppi_clauses == NIL) &&
-		innerrel->lateral_vars == NIL)
+		innerrel->lateral_vars == NIL &&
+		ph_lateral_vars == NIL)
 		return NULL;
 
 	/*
@@ -694,6 +770,7 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel,
 									outerrel->top_parent ?
 									outerrel->top_parent : outerrel,
 									innerrel,
+									ph_lateral_vars,
 									&param_exprs,
 									&hash_operators,
 									&binary_mode))
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index cf6886a288..933d7cf448 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -129,6 +129,74 @@ WHERE t1.unique1 < 10;
     20 | 0.50000000000000000000
 (1 row)
 
+-- Try with LATERAL references within PlaceHolderVars at a baserel
+SELECT explain_memoize('
+SELECT COUNT(*),AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty as c1, t2.unique1 as c2 FROM tenk1 t2) s on true
+WHERE s.c1 = s.c2 and t1.unique1 < 1000;', false);
+                                      explain_memoize                                      
+-------------------------------------------------------------------------------------------
+ Aggregate (actual rows=1 loops=N)
+   ->  Nested Loop (actual rows=1000 loops=N)
+         ->  Seq Scan on tenk1 t1 (actual rows=1000 loops=N)
+               Filter: (unique1 < 1000)
+               Rows Removed by Filter: 9000
+         ->  Memoize (actual rows=1 loops=N)
+               Cache Key: t1.twenty
+               Cache Mode: binary
+               Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
+               ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Filter: (t1.twenty = unique1)
+                     Rows Removed by Filter: 9999
+                     Heap Fetches: N
+(13 rows)
+
+-- And check we get the expected results.
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty as c1, t2.unique1 as c2 FROM tenk1 t2) s on true
+WHERE s.c1 = s.c2 and t1.unique1 < 1000;
+ count |        avg         
+-------+--------------------
+  1000 | 9.5000000000000000
+(1 row)
+
+-- Try with LATERAL references within PlaceHolderVars at a joinrel
+SELECT explain_memoize('
+SELECT COUNT(*),AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty as c1, t2.unique1 as c2 FROM tenk1 t2, tenk1 t3
+         WHERE t3.unique1 = 1) s on true
+WHERE s.c1 = s.c2 and t1.unique1 < 1000;', false);
+                                           explain_memoize                                           
+-----------------------------------------------------------------------------------------------------
+ Aggregate (actual rows=1 loops=N)
+   ->  Nested Loop (actual rows=1000 loops=N)
+         ->  Seq Scan on tenk1 t1 (actual rows=1000 loops=N)
+               Filter: (unique1 < 1000)
+               Rows Removed by Filter: 9000
+         ->  Memoize (actual rows=1 loops=N)
+               Cache Key: t1.twenty
+               Cache Mode: binary
+               Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
+               ->  Nested Loop (actual rows=1 loops=N)
+                     Join Filter: (t1.twenty = t2.unique1)
+                     Rows Removed by Join Filter: 9999
+                     ->  Index Only Scan using tenk1_unique1 on tenk1 t3 (actual rows=1 loops=N)
+                           Index Cond: (unique1 = 1)
+                           Heap Fetches: N
+                     ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=10000 loops=N)
+                           Heap Fetches: N
+(17 rows)
+
+-- And check we get the expected results.
+SELECT COUNT(*),AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty as c1, t2.unique1 as c2 FROM tenk1 t2, tenk1 t3
+         WHERE t3.unique1 = 1) s on true
+WHERE s.c1 = s.c2 and t1.unique1 < 1000;
+ count |        avg         
+-------+--------------------
+  1000 | 9.5000000000000000
+(1 row)
+
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
 SET work_mem TO '64kB';
 SET hash_mem_multiplier TO 1.0;
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index 1f4ab0ba3b..1ec96a520c 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -74,6 +74,30 @@ LATERAL (
 ON t1.two = t2.two
 WHERE t1.unique1 < 10;
 
+-- Try with LATERAL references within PlaceHolderVars at a baserel
+SELECT explain_memoize('
+SELECT COUNT(*),AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty as c1, t2.unique1 as c2 FROM tenk1 t2) s on true
+WHERE s.c1 = s.c2 and t1.unique1 < 1000;', false);
+
+-- And check we get the expected results.
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty as c1, t2.unique1 as c2 FROM tenk1 t2) s on true
+WHERE s.c1 = s.c2 and t1.unique1 < 1000;
+
+-- Try with LATERAL references within PlaceHolderVars at a joinrel
+SELECT explain_memoize('
+SELECT COUNT(*),AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty as c1, t2.unique1 as c2 FROM tenk1 t2, tenk1 t3
+         WHERE t3.unique1 = 1) s on true
+WHERE s.c1 = s.c2 and t1.unique1 < 1000;', false);
+
+-- And check we get the expected results.
+SELECT COUNT(*),AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty as c1, t2.unique1 as c2 FROM tenk1 t2, tenk1 t3
+         WHERE t3.unique1 = 1) s on true
+WHERE s.c1 = s.c2 and t1.unique1 < 1000;
+
 -- Reduce work_mem and hash_mem_multiplier so that we see some cache evictions
 SET work_mem TO '64kB';
 SET hash_mem_multiplier TO 1.0;
-- 
2.31.0



^ permalink  raw  reply  [nested|flat] 17+ messages in thread

* Re: Check lateral references within PHVs for memoize cache keys
  2023-12-25 07:01 Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  2024-02-02 09:18 ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
@ 2024-03-18 08:36   ` Richard Guo <[email protected]>
  2024-06-18 01:47     ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  0 siblings, 1 reply; 17+ messages in thread

From: Richard Guo @ 2024-03-18 08:36 UTC (permalink / raw)
  To: Tom Lane <[email protected]>; +Cc: pgsql-hackers; David Rowley <[email protected]>

On Fri, Feb 2, 2024 at 5:18 PM Richard Guo <[email protected]> wrote:

> The v4 patch does not apply any more.  I've rebased it on master.
> Nothing else has changed.
>

Here is another rebase over master so it applies again.  I also added a
commit message to help review.  Nothing else has changed.

Thanks
Richard


Attachments:

  [application/octet-stream] v6-0001-Check-lateral-refs-within-PHVs-for-memoize-cache-keys.patch (12.0K, ../../CAMbWs48A3Zd2=wZ9xCM07qHEeGBw-X2nodj2n_qnHNtBqiA73g@mail.gmail.com/3-v6-0001-Check-lateral-refs-within-PHVs-for-memoize-cache-keys.patch)
  download | inline diff:
From 858c40a173444c7965d6313c12734996215b4d6b Mon Sep 17 00:00:00 2001
From: Richard Guo <[email protected]>
Date: Fri, 30 Dec 2022 10:35:36 +0800
Subject: [PATCH v6] Check lateral refs within PHVs for memoize cache keys

If we intend to generate a memoize node on top of a path, we need cache
keys of some sort.  Currently we search for the cache keys in the
parameterized clauses of the path as well as the lateral_vars of its
parent.  However, it turns out that this is not sufficient because their
might be lateral references derived from PlaceHolderVars, which we fail
to take into consideration.

This oversight can cause us to miss opportunities to utilize the Memoize
node.  Moreover, in some plans, the failure to recognize all the cache
keys could result in performance regressions.  This is because without
identifying all the cache keys, we would need to purge the entire cache
every time we get a new outer tuple during execution.

This patch fixes this issue by extracting lateral Vars from within
PlaceHolderVars and subsequently including them in the cache keys.
---
 .../postgres_fdw/expected/postgres_fdw.out    | 12 ++-
 src/backend/optimizer/path/joinpath.c         | 85 ++++++++++++++++++-
 src/test/regress/expected/memoize.out         | 68 +++++++++++++++
 src/test/regress/sql/memoize.sql              | 24 ++++++
 4 files changed, 181 insertions(+), 8 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 58a603ac56..b3d087a110 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -3737,15 +3737,19 @@ ORDER BY ref_0."C 1";
          ->  Index Scan using t1_pkey on "S 1"."T 1" ref_0
                Output: ref_0."C 1", ref_0.c2, ref_0.c3, ref_0.c4, ref_0.c5, ref_0.c6, ref_0.c7, ref_0.c8
                Index Cond: (ref_0."C 1" < 10)
-         ->  Foreign Scan on public.ft1 ref_1
-               Output: ref_1.c3, ref_0.c2
-               Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE ((c3 = '00001'))
+         ->  Memoize
+               Output: ref_1.c3, (ref_0.c2)
+               Cache Key: ref_0.c2
+               Cache Mode: binary
+               ->  Foreign Scan on public.ft1 ref_1
+                     Output: ref_1.c3, ref_0.c2
+                     Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE ((c3 = '00001'))
    ->  Materialize
          Output: ref_3.c3
          ->  Foreign Scan on public.ft2 ref_3
                Output: ref_3.c3
                Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE ((c3 = '00001'))
-(15 rows)
+(19 rows)
 
 SELECT ref_0.c2, subq_1.*
 FROM
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 6aca66f196..e4dae8e534 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -23,6 +23,7 @@
 #include "optimizer/optimizer.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
+#include "optimizer/placeholder.h"
 #include "optimizer/planmain.h"
 #include "utils/typcache.h"
 
@@ -437,10 +438,11 @@ have_unsafe_outer_join_ref(PlannerInfo *root,
 static bool
 paraminfo_get_equal_hashops(PlannerInfo *root, ParamPathInfo *param_info,
 							RelOptInfo *outerrel, RelOptInfo *innerrel,
-							List **param_exprs, List **operators,
-							bool *binary_mode)
+							List *ph_lateral_vars, List **param_exprs,
+							List **operators, bool *binary_mode)
 
 {
+	List	   *lateral_vars;
 	ListCell   *lc;
 
 	*param_exprs = NIL;
@@ -520,7 +522,8 @@ paraminfo_get_equal_hashops(PlannerInfo *root, ParamPathInfo *param_info,
 	}
 
 	/* Now add any lateral vars to the cache key too */
-	foreach(lc, innerrel->lateral_vars)
+	lateral_vars = list_concat(ph_lateral_vars, innerrel->lateral_vars);
+	foreach(lc, lateral_vars)
 	{
 		Node	   *expr = (Node *) lfirst(lc);
 		TypeCacheEntry *typentry;
@@ -571,6 +574,71 @@ paraminfo_get_equal_hashops(PlannerInfo *root, ParamPathInfo *param_info,
 	return true;
 }
 
+/*
+ * extract_lateral_vars_from_PHVs
+ *	  Extract lateral references within PlaceHolderVars that are due to be
+ *	  evaluated at 'innerrelids'.
+ */
+static List *
+extract_lateral_vars_from_PHVs(PlannerInfo *root, Relids innerrelids)
+{
+	List	   *ph_lateral_vars = NIL;
+	ListCell   *lc;
+
+	/* Nothing would be found if the query contains no LATERAL RTEs */
+	if (!root->hasLateralRTEs)
+		return NIL;
+
+	foreach(lc, root->placeholder_list)
+	{
+		PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(lc);
+		List	   *vars;
+		ListCell   *cell;
+
+		/* PHV is uninteresting if no lateral refs */
+		if (phinfo->ph_lateral == NULL)
+			continue;
+
+		/* PHV is uninteresting if not due to be evaluated at innerrelids */
+		if (!bms_equal(phinfo->ph_eval_at, innerrelids))
+			continue;
+
+		/* Fetch Vars and PHVs of lateral references within PlaceHolderVars */
+		vars = pull_vars_of_level((Node *) phinfo->ph_var->phexpr, 0);
+		foreach(cell, vars)
+		{
+			Node	   *node = (Node *) lfirst(cell);
+
+			node = copyObject(node);
+			if (IsA(node, Var))
+			{
+				Var		   *var = (Var *) node;
+
+				Assert(var->varlevelsup == 0);
+
+				if (bms_is_member(var->varno, phinfo->ph_lateral))
+					ph_lateral_vars = lappend(ph_lateral_vars, node);
+			}
+			else if (IsA(node, PlaceHolderVar))
+			{
+				PlaceHolderVar *phv = (PlaceHolderVar *) node;
+
+				Assert(phv->phlevelsup == 0);
+
+				if (bms_is_subset(find_placeholder_info(root, phv)->ph_eval_at,
+								  phinfo->ph_lateral))
+					ph_lateral_vars = lappend(ph_lateral_vars, node);
+			}
+			else
+				Assert(false);
+		}
+
+		list_free(vars);
+	}
+
+	return ph_lateral_vars;
+}
+
 /*
  * get_memoize_path
  *		If possible, make and return a Memoize path atop of 'inner_path'.
@@ -586,6 +654,7 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel,
 	List	   *hash_operators;
 	ListCell   *lc;
 	bool		binary_mode;
+	List	   *ph_lateral_vars;
 
 	/* Obviously not if it's disabled */
 	if (!enable_memoize)
@@ -600,6 +669,12 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel,
 	if (outer_path->parent->rows < 2)
 		return NULL;
 
+	/*
+	 * Extract lateral Vars within PlaceHolderVars that are due to be evaluated
+	 * at innerrel.  These lateral Vars could be used as memoize cache keys.
+	 */
+	ph_lateral_vars = extract_lateral_vars_from_PHVs(root, innerrel->relids);
+
 	/*
 	 * We can only have a memoize node when there's some kind of cache key,
 	 * either parameterized path clauses or lateral Vars.  No cache key sounds
@@ -607,7 +682,8 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel,
 	 */
 	if ((inner_path->param_info == NULL ||
 		 inner_path->param_info->ppi_clauses == NIL) &&
-		innerrel->lateral_vars == NIL)
+		innerrel->lateral_vars == NIL &&
+		ph_lateral_vars == NIL)
 		return NULL;
 
 	/*
@@ -694,6 +770,7 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel,
 									outerrel->top_parent ?
 									outerrel->top_parent : outerrel,
 									innerrel,
+									ph_lateral_vars,
 									&param_exprs,
 									&hash_operators,
 									&binary_mode))
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index 1c8d996740..c2e9a9d626 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -129,6 +129,74 @@ WHERE t1.unique1 < 10;
     20 | 0.50000000000000000000
 (1 row)
 
+-- Try with LATERAL references within PlaceHolderVars at a baserel
+SELECT explain_memoize('
+SELECT COUNT(*),AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty as c1, t2.unique1 as c2 FROM tenk1 t2) s on true
+WHERE s.c1 = s.c2 and t1.unique1 < 1000;', false);
+                                      explain_memoize                                      
+-------------------------------------------------------------------------------------------
+ Aggregate (actual rows=1 loops=N)
+   ->  Nested Loop (actual rows=1000 loops=N)
+         ->  Seq Scan on tenk1 t1 (actual rows=1000 loops=N)
+               Filter: (unique1 < 1000)
+               Rows Removed by Filter: 9000
+         ->  Memoize (actual rows=1 loops=N)
+               Cache Key: t1.twenty
+               Cache Mode: binary
+               Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
+               ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Filter: (t1.twenty = unique1)
+                     Rows Removed by Filter: 9999
+                     Heap Fetches: N
+(13 rows)
+
+-- And check we get the expected results.
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty as c1, t2.unique1 as c2 FROM tenk1 t2) s on true
+WHERE s.c1 = s.c2 and t1.unique1 < 1000;
+ count |        avg         
+-------+--------------------
+  1000 | 9.5000000000000000
+(1 row)
+
+-- Try with LATERAL references within PlaceHolderVars at a joinrel
+SELECT explain_memoize('
+SELECT COUNT(*),AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty as c1, t2.unique1 as c2 FROM tenk1 t2, tenk1 t3
+         WHERE t3.unique1 = 1) s on true
+WHERE s.c1 = s.c2 and t1.unique1 < 1000;', false);
+                                           explain_memoize                                           
+-----------------------------------------------------------------------------------------------------
+ Aggregate (actual rows=1 loops=N)
+   ->  Nested Loop (actual rows=1000 loops=N)
+         ->  Seq Scan on tenk1 t1 (actual rows=1000 loops=N)
+               Filter: (unique1 < 1000)
+               Rows Removed by Filter: 9000
+         ->  Memoize (actual rows=1 loops=N)
+               Cache Key: t1.twenty
+               Cache Mode: binary
+               Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
+               ->  Nested Loop (actual rows=1 loops=N)
+                     Join Filter: (t1.twenty = t2.unique1)
+                     Rows Removed by Join Filter: 9999
+                     ->  Index Only Scan using tenk1_unique1 on tenk1 t3 (actual rows=1 loops=N)
+                           Index Cond: (unique1 = 1)
+                           Heap Fetches: N
+                     ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=10000 loops=N)
+                           Heap Fetches: N
+(17 rows)
+
+-- And check we get the expected results.
+SELECT COUNT(*),AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty as c1, t2.unique1 as c2 FROM tenk1 t2, tenk1 t3
+         WHERE t3.unique1 = 1) s on true
+WHERE s.c1 = s.c2 and t1.unique1 < 1000;
+ count |        avg         
+-------+--------------------
+  1000 | 9.5000000000000000
+(1 row)
+
 SET enable_mergejoin TO off;
 -- Test for varlena datatype with expr evaluation
 CREATE TABLE expr_key (x numeric, t text);
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index e00e1a94a8..a43f3a713a 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -74,6 +74,30 @@ LATERAL (
 ON t1.two = t2.two
 WHERE t1.unique1 < 10;
 
+-- Try with LATERAL references within PlaceHolderVars at a baserel
+SELECT explain_memoize('
+SELECT COUNT(*),AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty as c1, t2.unique1 as c2 FROM tenk1 t2) s on true
+WHERE s.c1 = s.c2 and t1.unique1 < 1000;', false);
+
+-- And check we get the expected results.
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty as c1, t2.unique1 as c2 FROM tenk1 t2) s on true
+WHERE s.c1 = s.c2 and t1.unique1 < 1000;
+
+-- Try with LATERAL references within PlaceHolderVars at a joinrel
+SELECT explain_memoize('
+SELECT COUNT(*),AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty as c1, t2.unique1 as c2 FROM tenk1 t2, tenk1 t3
+         WHERE t3.unique1 = 1) s on true
+WHERE s.c1 = s.c2 and t1.unique1 < 1000;', false);
+
+-- And check we get the expected results.
+SELECT COUNT(*),AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty as c1, t2.unique1 as c2 FROM tenk1 t2, tenk1 t3
+         WHERE t3.unique1 = 1) s on true
+WHERE s.c1 = s.c2 and t1.unique1 < 1000;
+
 SET enable_mergejoin TO off;
 
 -- Test for varlena datatype with expr evaluation
-- 
2.31.0



^ permalink  raw  reply  [nested|flat] 17+ messages in thread

* Re: Check lateral references within PHVs for memoize cache keys
  2023-12-25 07:01 Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  2024-02-02 09:18 ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  2024-03-18 08:36   ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
@ 2024-06-18 01:47     ` Richard Guo <[email protected]>
  2024-06-28 14:14       ` Re: Check lateral references within PHVs for memoize cache keys Andrei Lepikhov <[email protected]>
  0 siblings, 1 reply; 17+ messages in thread

From: Richard Guo @ 2024-06-18 01:47 UTC (permalink / raw)
  To: Tom Lane <[email protected]>; +Cc: pgsql-hackers; David Rowley <[email protected]>

On Mon, Mar 18, 2024 at 4:36 PM Richard Guo <[email protected]> wrote:
> Here is another rebase over master so it applies again.  I also added a
> commit message to help review.  Nothing else has changed.

AFAIU currently we do not add Memoize nodes on top of join relation
paths.  This is because the ParamPathInfos for join relation paths do
not maintain ppi_clauses, as the set of relevant clauses varies
depending on how the join is formed.  In addition, joinrels do not
maintain lateral_vars.  So we do not have a way to extract cache keys
from joinrels.

(Besides, there are places where the code doesn't cope with Memoize path
on top of a joinrel path, such as in get_param_path_clause_serials.)

Therefore, when extracting lateral references within PlaceHolderVars,
there is no need to consider those that are due to be evaluated at
joinrels.

Hence, here is v7 patch for that.  In passing, this patch also includes
a comment explaining that Memoize nodes are currently not added on top
of join relation paths (maybe we should have a separate patch for this?).

Thanks
Richard


Attachments:

  [application/octet-stream] v7-0001-Check-lateral-refs-within-PHVs-for-memoize-cache-keys.patch (12.5K, ../../CAMbWs4-n-bc=cxXwn7oY0E8YojSwiVLUpz6+OqPre3_YJs1_NQ@mail.gmail.com/2-v7-0001-Check-lateral-refs-within-PHVs-for-memoize-cache-keys.patch)
  download | inline diff:
From 68c8838061213da8abdd54a602da40c7ed5c98a8 Mon Sep 17 00:00:00 2001
From: Richard Guo <[email protected]>
Date: Mon, 17 Jun 2024 12:32:00 +0900
Subject: [PATCH v7] Check lateral refs within PHVs for memoize cache keys

If we intend to generate a memoize node on top of a path, we need cache
keys of some sort.  Currently we search for the cache keys in the
parameterized clauses of the path as well as the lateral_vars of its
parent.  However, it turns out that this is not sufficient because their
might be lateral references derived from PlaceHolderVars, which we fail
to take into consideration.

This oversight can cause us to miss opportunities to utilize the Memoize
node.  Moreover, in some plans, the failure to recognize all the cache
keys could result in performance regressions.  This is because without
identifying all the cache keys, we would need to purge the entire cache
every time we get a new outer tuple during execution.

This patch fixes this issue by extracting lateral Vars from within
PlaceHolderVars and subsequently including them in the cache keys.

This patch also includes a comment clarifying that Memoize nodes are
currently not added on top of join relation paths.  This explains why
this patch only considers PHVs that are due to be evaluated at baserels.
---
 .../postgres_fdw/expected/postgres_fdw.out    | 12 ++-
 src/backend/optimizer/path/joinpath.c         | 98 ++++++++++++++++++-
 src/test/regress/expected/memoize.out         | 63 ++++++++++++
 src/test/regress/sql/memoize.sql              | 24 +++++
 4 files changed, 189 insertions(+), 8 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index ea566d5034..8cf222caca 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -3774,15 +3774,19 @@ ORDER BY ref_0."C 1";
          ->  Index Scan using t1_pkey on "S 1"."T 1" ref_0
                Output: ref_0."C 1", ref_0.c2, ref_0.c3, ref_0.c4, ref_0.c5, ref_0.c6, ref_0.c7, ref_0.c8
                Index Cond: (ref_0."C 1" < 10)
-         ->  Foreign Scan on public.ft1 ref_1
-               Output: ref_1.c3, ref_0.c2
-               Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE ((c3 = '00001'))
+         ->  Memoize
+               Output: ref_1.c3, (ref_0.c2)
+               Cache Key: ref_0.c2
+               Cache Mode: binary
+               ->  Foreign Scan on public.ft1 ref_1
+                     Output: ref_1.c3, ref_0.c2
+                     Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE ((c3 = '00001'))
    ->  Materialize
          Output: ref_3.c3
          ->  Foreign Scan on public.ft2 ref_3
                Output: ref_3.c3
                Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE ((c3 = '00001'))
-(15 rows)
+(19 rows)
 
 SELECT ref_0.c2, subq_1.*
 FROM
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 5be8da9e09..eb61bb096d 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -23,6 +23,7 @@
 #include "optimizer/optimizer.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
+#include "optimizer/placeholder.h"
 #include "optimizer/planmain.h"
 #include "utils/typcache.h"
 
@@ -438,10 +439,11 @@ have_unsafe_outer_join_ref(PlannerInfo *root,
 static bool
 paraminfo_get_equal_hashops(PlannerInfo *root, ParamPathInfo *param_info,
 							RelOptInfo *outerrel, RelOptInfo *innerrel,
-							List **param_exprs, List **operators,
-							bool *binary_mode)
+							List *ph_lateral_vars, List **param_exprs,
+							List **operators, bool *binary_mode)
 
 {
+	List	   *lateral_vars;
 	ListCell   *lc;
 
 	*param_exprs = NIL;
@@ -521,7 +523,8 @@ paraminfo_get_equal_hashops(PlannerInfo *root, ParamPathInfo *param_info,
 	}
 
 	/* Now add any lateral vars to the cache key too */
-	foreach(lc, innerrel->lateral_vars)
+	lateral_vars = list_concat(ph_lateral_vars, innerrel->lateral_vars);
+	foreach(lc, lateral_vars)
 	{
 		Node	   *expr = (Node *) lfirst(lc);
 		TypeCacheEntry *typentry;
@@ -572,10 +575,88 @@ paraminfo_get_equal_hashops(PlannerInfo *root, ParamPathInfo *param_info,
 	return true;
 }
 
+/*
+ * extract_lateral_vars_from_PHVs
+ *	  Extract lateral references within PlaceHolderVars that are due to be
+ *	  evaluated at 'innerrelids'.
+ */
+static List *
+extract_lateral_vars_from_PHVs(PlannerInfo *root, Relids innerrelids)
+{
+	List	   *ph_lateral_vars = NIL;
+	ListCell   *lc;
+
+	/* Nothing would be found if the query contains no LATERAL RTEs */
+	if (!root->hasLateralRTEs)
+		return NIL;
+
+	/*
+	 * No need to consider PHVs that are due to be evaluated at joinrels, since
+	 * we do not add Memoize nodes on top of joinrel paths.
+	 */
+	if (bms_membership(innerrelids) == BMS_MULTIPLE)
+		return NIL;
+
+	foreach(lc, root->placeholder_list)
+	{
+		PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(lc);
+		List	   *vars;
+		ListCell   *cell;
+
+		/* PHV is uninteresting if no lateral refs */
+		if (phinfo->ph_lateral == NULL)
+			continue;
+
+		/* PHV is uninteresting if not due to be evaluated at innerrelids */
+		if (!bms_equal(phinfo->ph_eval_at, innerrelids))
+			continue;
+
+		/* Fetch Vars and PHVs of lateral references within PlaceHolderVars */
+		vars = pull_vars_of_level((Node *) phinfo->ph_var->phexpr, 0);
+		foreach(cell, vars)
+		{
+			Node	   *node = (Node *) lfirst(cell);
+
+			node = copyObject(node);
+			if (IsA(node, Var))
+			{
+				Var		   *var = (Var *) node;
+
+				Assert(var->varlevelsup == 0);
+
+				if (bms_is_member(var->varno, phinfo->ph_lateral))
+					ph_lateral_vars = lappend(ph_lateral_vars, node);
+			}
+			else if (IsA(node, PlaceHolderVar))
+			{
+				PlaceHolderVar *phv = (PlaceHolderVar *) node;
+
+				Assert(phv->phlevelsup == 0);
+
+				if (bms_is_subset(find_placeholder_info(root, phv)->ph_eval_at,
+								  phinfo->ph_lateral))
+					ph_lateral_vars = lappend(ph_lateral_vars, node);
+			}
+			else
+				Assert(false);
+		}
+
+		list_free(vars);
+	}
+
+	return ph_lateral_vars;
+}
+
 /*
  * get_memoize_path
  *		If possible, make and return a Memoize path atop of 'inner_path'.
  *		Otherwise return NULL.
+ *
+ * Note that currently we do not add Memoize nodes on top of join relation
+ * paths.  This is because the ParamPathInfos for join relation paths do not
+ * maintain ppi_clauses, as the set of relevant clauses varies depending on how
+ * the join is formed.  In addition, joinrels do not maintain lateral_vars.  So
+ * we do not have a way to extract cache keys from joinrels.
  */
 static Path *
 get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel,
@@ -587,6 +668,7 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel,
 	List	   *hash_operators;
 	ListCell   *lc;
 	bool		binary_mode;
+	List	   *ph_lateral_vars;
 
 	/* Obviously not if it's disabled */
 	if (!enable_memoize)
@@ -601,6 +683,12 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel,
 	if (outer_path->parent->rows < 2)
 		return NULL;
 
+	/*
+	 * Extract lateral Vars within PlaceHolderVars that are due to be evaluated
+	 * at innerrel.  These lateral Vars could be used as memoize cache keys.
+	 */
+	ph_lateral_vars = extract_lateral_vars_from_PHVs(root, innerrel->relids);
+
 	/*
 	 * We can only have a memoize node when there's some kind of cache key,
 	 * either parameterized path clauses or lateral Vars.  No cache key sounds
@@ -608,7 +696,8 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel,
 	 */
 	if ((inner_path->param_info == NULL ||
 		 inner_path->param_info->ppi_clauses == NIL) &&
-		innerrel->lateral_vars == NIL)
+		innerrel->lateral_vars == NIL &&
+		ph_lateral_vars == NIL)
 		return NULL;
 
 	/*
@@ -695,6 +784,7 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel,
 									outerrel->top_parent ?
 									outerrel->top_parent : outerrel,
 									innerrel,
+									ph_lateral_vars,
 									&param_exprs,
 									&hash_operators,
 									&binary_mode))
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index 0fd103c06b..4c2a37bf97 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -129,6 +129,69 @@ WHERE t1.unique1 < 10;
     20 | 0.50000000000000000000
 (1 row)
 
+-- Try with LATERAL references within PlaceHolderVars at a baserel
+SELECT explain_memoize('
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty AS c1, t2.unique1 AS c2 FROM tenk1 t2) s ON TRUE
+WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
+                                      explain_memoize                                      
+-------------------------------------------------------------------------------------------
+ Aggregate (actual rows=1 loops=N)
+   ->  Nested Loop (actual rows=1000 loops=N)
+         ->  Seq Scan on tenk1 t1 (actual rows=1000 loops=N)
+               Filter: (unique1 < 1000)
+               Rows Removed by Filter: 9000
+         ->  Memoize (actual rows=1 loops=N)
+               Cache Key: t1.twenty
+               Cache Mode: binary
+               Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
+               ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Filter: (t1.twenty = unique1)
+                     Rows Removed by Filter: 9999
+                     Heap Fetches: N
+(13 rows)
+
+-- And check we get the expected results.
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty AS c1, t2.unique1 AS c2 FROM tenk1 t2) s ON TRUE
+WHERE s.c1 = s.c2 AND t1.unique1 < 1000;
+ count |        avg         
+-------+--------------------
+  1000 | 9.5000000000000000
+(1 row)
+
+-- Ensure we do not omit the cache keys from PlaceHolderVars
+SELECT explain_memoize('
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty AS c1, t2.unique1 AS c2, t2.two FROM tenk1 t2) s
+ON t1.two = s.two
+WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
+                                    explain_memoize                                    
+---------------------------------------------------------------------------------------
+ Aggregate (actual rows=1 loops=N)
+   ->  Nested Loop (actual rows=1000 loops=N)
+         ->  Seq Scan on tenk1 t1 (actual rows=1000 loops=N)
+               Filter: (unique1 < 1000)
+               Rows Removed by Filter: 9000
+         ->  Memoize (actual rows=1 loops=N)
+               Cache Key: t1.two, t1.twenty
+               Cache Mode: binary
+               Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
+               ->  Seq Scan on tenk1 t2 (actual rows=1 loops=N)
+                     Filter: ((t1.twenty = unique1) AND (t1.two = two))
+                     Rows Removed by Filter: 9999
+(12 rows)
+
+-- And check we get the expected results.
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty AS c1, t2.unique1 AS c2, t2.two FROM tenk1 t2) s
+ON t1.two = s.two
+WHERE s.c1 = s.c2 AND t1.unique1 < 1000;
+ count |        avg         
+-------+--------------------
+  1000 | 9.5000000000000000
+(1 row)
+
 SET enable_mergejoin TO off;
 -- Test for varlena datatype with expr evaluation
 CREATE TABLE expr_key (x numeric, t text);
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index e00e1a94a8..ff1d771bd2 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -74,6 +74,30 @@ LATERAL (
 ON t1.two = t2.two
 WHERE t1.unique1 < 10;
 
+-- Try with LATERAL references within PlaceHolderVars at a baserel
+SELECT explain_memoize('
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty AS c1, t2.unique1 AS c2 FROM tenk1 t2) s ON TRUE
+WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
+
+-- And check we get the expected results.
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty AS c1, t2.unique1 AS c2 FROM tenk1 t2) s ON TRUE
+WHERE s.c1 = s.c2 AND t1.unique1 < 1000;
+
+-- Ensure we do not omit the cache keys from PlaceHolderVars
+SELECT explain_memoize('
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty AS c1, t2.unique1 AS c2, t2.two FROM tenk1 t2) s
+ON t1.two = s.two
+WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
+
+-- And check we get the expected results.
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty AS c1, t2.unique1 AS c2, t2.two FROM tenk1 t2) s
+ON t1.two = s.two
+WHERE s.c1 = s.c2 AND t1.unique1 < 1000;
+
 SET enable_mergejoin TO off;
 
 -- Test for varlena datatype with expr evaluation
-- 
2.43.0



^ permalink  raw  reply  [nested|flat] 17+ messages in thread

* Re: Check lateral references within PHVs for memoize cache keys
  2023-12-25 07:01 Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  2024-02-02 09:18 ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  2024-03-18 08:36   ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  2024-06-18 01:47     ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
@ 2024-06-28 14:14       ` Andrei Lepikhov <[email protected]>
  2024-07-11 09:18         ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  0 siblings, 1 reply; 17+ messages in thread

From: Andrei Lepikhov @ 2024-06-28 14:14 UTC (permalink / raw)
  To: Richard Guo <[email protected]>; +Cc: pgsql-hackers; David Rowley <[email protected]>; Tom Lane <[email protected]>

On 6/18/24 08:47, Richard Guo wrote:
> On Mon, Mar 18, 2024 at 4:36 PM Richard Guo <[email protected]> wrote:
>> Here is another rebase over master so it applies again.  I also added a
>> commit message to help review.  Nothing else has changed.
> 
> AFAIU currently we do not add Memoize nodes on top of join relation
> paths.  This is because the ParamPathInfos for join relation paths do
> not maintain ppi_clauses, as the set of relevant clauses varies
> depending on how the join is formed.  In addition, joinrels do not
> maintain lateral_vars.  So we do not have a way to extract cache keys
> from joinrels.
> 
> (Besides, there are places where the code doesn't cope with Memoize path
> on top of a joinrel path, such as in get_param_path_clause_serials.)
> 
> Therefore, when extracting lateral references within PlaceHolderVars,
> there is no need to consider those that are due to be evaluated at
> joinrels.
> 
> Hence, here is v7 patch for that.  In passing, this patch also includes
> a comment explaining that Memoize nodes are currently not added on top
> of join relation paths (maybe we should have a separate patch for this?).
Hi,
I have reviewed v7 of the patch. This improvement is good enough to be 
applied, thought. Here is some notes:

Comment may be rewritten for clarity:
"Determine if the clauses in param_info and innerrel's lateral_vars" -
I'd replace lateral_vars with 'lateral references' to combine in one 
phrase PHV from rel and root->placeholder_list sources.

I wonder if we can add whole PHV expression instead of the Var (as 
discussed above) just under some condition:
if (!bms_intersect(pull_varnos(root, (Node *) phinfo->ph_var->phexpr), 
innerrelids))
{
   // Add whole PHV
}
else
{
   // Add only pulled vars
}

I got the point about Memoize over join, but as a join still calls 
replace_nestloop_params to replace parameters in its clauses, why not to 
invent something similar to find Memoize keys inside specific JoinPath 
node? It is not the issue of this patch, though - but is it doable?

IMO, the code:
if (bms_nonempty_difference(outerPlan->chgParam, node->keyparamids))
   cache_purge_all(node);

is a good place to check an assertion: is it really the parent query 
parameters that make a difference between memoize keys and node list of 
parameters?

Generally, this patch looks good for me to be committed.

-- 
regards, Andrei Lepikhov







^ permalink  raw  reply  [nested|flat] 17+ messages in thread

* Re: Check lateral references within PHVs for memoize cache keys
  2023-12-25 07:01 Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  2024-02-02 09:18 ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  2024-03-18 08:36   ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  2024-06-18 01:47     ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  2024-06-28 14:14       ` Re: Check lateral references within PHVs for memoize cache keys Andrei Lepikhov <[email protected]>
@ 2024-07-11 09:18         ` Richard Guo <[email protected]>
  2024-07-12 03:18           ` Re: Check lateral references within PHVs for memoize cache keys Andrei Lepikhov <[email protected]>
  2024-07-15 01:36           ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  0 siblings, 2 replies; 17+ messages in thread

From: Richard Guo @ 2024-07-11 09:18 UTC (permalink / raw)
  To: Andrei Lepikhov <[email protected]>; +Cc: pgsql-hackers; David Rowley <[email protected]>; Tom Lane <[email protected]>

On Fri, Jun 28, 2024 at 10:14 PM Andrei Lepikhov <[email protected]> wrote:
> I have reviewed v7 of the patch. This improvement is good enough to be
> applied, thought.

Thank you for reviewing this patch!

> Comment may be rewritten for clarity:
> "Determine if the clauses in param_info and innerrel's lateral_vars" -
> I'd replace lateral_vars with 'lateral references' to combine in one
> phrase PHV from rel and root->placeholder_list sources.

Makes sense.  I ended up using 'innerrel's lateral vars' to include
both the lateral Vars/PHVs found in innerrel->lateral_vars and those
extracted from within PlaceHolderVars that are due to be evaluated at
innerrel.

> I wonder if we can add whole PHV expression instead of the Var (as
> discussed above) just under some condition:
> if (!bms_intersect(pull_varnos(root, (Node *) phinfo->ph_var->phexpr),
> innerrelids))
> {
>    // Add whole PHV
> }
> else
> {
>    // Add only pulled vars
> }

Good point.  After considering it further, I think we should do this.
As David explained, this can be beneficial in cases where the whole
expression results in fewer distinct values to cache tuples for.

> I got the point about Memoize over join, but as a join still calls
> replace_nestloop_params to replace parameters in its clauses, why not to
> invent something similar to find Memoize keys inside specific JoinPath
> node? It is not the issue of this patch, though - but is it doable?

I don't think it's impossible to do, but I'm skeptical that there's an
easy way to identify all the cache keys for joinrels, without having
available ppi_clauses and lateral_vars.

> IMO, the code:
> if (bms_nonempty_difference(outerPlan->chgParam, node->keyparamids))
>    cache_purge_all(node);
>
> is a good place to check an assertion: is it really the parent query
> parameters that make a difference between memoize keys and node list of
> parameters?

I don't think we have enough info available here to identify which
params within outerPlan->chgParam are from outer levels.  Maybe we can
store root->outer_params in the MemoizeState node to help with this
assertion, but I'm not sure if it's worth the trouble.

Attached is an updated version of this patch.

Thanks
Richard


Attachments:

  [application/octet-stream] v8-0001-Check-lateral-references-within-PHVs-for-memoize-cache-keys.patch (15.3K, ../../CAMbWs4_CQR9ikyAhjH0CwzCdp+FcRAeoVsi5Oohd7s=c5RZABg@mail.gmail.com/2-v8-0001-Check-lateral-references-within-PHVs-for-memoize-cache-keys.patch)
  download | inline diff:
From c81d69c606564397f500adff3a1648f742f89a9c Mon Sep 17 00:00:00 2001
From: Richard Guo <[email protected]>
Date: Mon, 17 Jun 2024 12:32:00 +0900
Subject: [PATCH v8] Check lateral references within PHVs for memoize cache
 keys

If we intend to generate a Memoize node on top of a path, we need
cache keys of some sort.  Currently we search for the cache keys in
the parameterized clauses of the path as well as the lateral_vars of
its parent.  However, it turns out that this is not sufficient because
there might be lateral references derived from PlaceHolderVars, which
we fail to take into consideration.

This oversight can cause us to miss opportunities to utilize the
Memoize node.  Moreover, in some plans, failing to recognize all the
cache keys could result in performance regressions.  This is because
without identifying all the cache keys, we would need to purge the
entire cache every time we get a new outer tuple during execution.

This patch fixes this issue by extracting lateral Vars from within
PlaceHolderVars and subsequently including them in the cache keys.

In passing, this patch also includes a comment clarifying that Memoize
nodes are currently not added on top of join relation paths.  This
explains why this patch only considers PlaceHolderVars that are due to
be evaluated at baserels.

Author: Richard Guo
Reviewed-by: Tom Lane, Andrei Lepikhov
Discussion: https://postgr.es/m/CAMbWs48jLxn0pAPZpJ50EThZ569Xrw+=4Ac3QvkpQvNszbeoNg@mail.gmail.com
---
 .../postgres_fdw/expected/postgres_fdw.out    |  12 +-
 src/backend/optimizer/path/joinpath.c         | 114 +++++++++++++++++-
 src/test/regress/expected/memoize.out         |  93 ++++++++++++++
 src/test/regress/sql/memoize.sql              |  35 ++++++
 4 files changed, 245 insertions(+), 9 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 1f22309194..0cc77190dc 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -3774,15 +3774,19 @@ ORDER BY ref_0."C 1";
          ->  Index Scan using t1_pkey on "S 1"."T 1" ref_0
                Output: ref_0."C 1", ref_0.c2, ref_0.c3, ref_0.c4, ref_0.c5, ref_0.c6, ref_0.c7, ref_0.c8
                Index Cond: (ref_0."C 1" < 10)
-         ->  Foreign Scan on public.ft1 ref_1
-               Output: ref_1.c3, ref_0.c2
-               Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE ((c3 = '00001'))
+         ->  Memoize
+               Output: ref_1.c3, (ref_0.c2)
+               Cache Key: ref_0.c2
+               Cache Mode: binary
+               ->  Foreign Scan on public.ft1 ref_1
+                     Output: ref_1.c3, ref_0.c2
+                     Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE ((c3 = '00001'))
    ->  Materialize
          Output: ref_3.c3
          ->  Foreign Scan on public.ft2 ref_3
                Output: ref_3.c3
                Remote SQL: SELECT c3 FROM "S 1"."T 1" WHERE ((c3 = '00001'))
-(15 rows)
+(19 rows)
 
 SELECT ref_0.c2, subq_1.*
 FROM
diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 40eb58341c..54be52e293 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -23,6 +23,7 @@
 #include "optimizer/optimizer.h"
 #include "optimizer/pathnode.h"
 #include "optimizer/paths.h"
+#include "optimizer/placeholder.h"
 #include "optimizer/planmain.h"
 #include "utils/typcache.h"
 
@@ -425,7 +426,7 @@ have_unsafe_outer_join_ref(PlannerInfo *root,
 
 /*
  * paraminfo_get_equal_hashops
- *		Determine if the clauses in param_info and innerrel's lateral_vars
+ *		Determine if the clauses in param_info and innerrel's lateral vars
  *		can be hashed.
  *		Returns true if hashing is possible, otherwise false.
  *
@@ -438,10 +439,11 @@ have_unsafe_outer_join_ref(PlannerInfo *root,
 static bool
 paraminfo_get_equal_hashops(PlannerInfo *root, ParamPathInfo *param_info,
 							RelOptInfo *outerrel, RelOptInfo *innerrel,
-							List **param_exprs, List **operators,
-							bool *binary_mode)
+							List *ph_lateral_vars, List **param_exprs,
+							List **operators, bool *binary_mode)
 
 {
+	List	   *lateral_vars;
 	ListCell   *lc;
 
 	*param_exprs = NIL;
@@ -521,7 +523,8 @@ paraminfo_get_equal_hashops(PlannerInfo *root, ParamPathInfo *param_info,
 	}
 
 	/* Now add any lateral vars to the cache key too */
-	foreach(lc, innerrel->lateral_vars)
+	lateral_vars = list_concat(ph_lateral_vars, innerrel->lateral_vars);
+	foreach(lc, lateral_vars)
 	{
 		Node	   *expr = (Node *) lfirst(lc);
 		TypeCacheEntry *typentry;
@@ -572,10 +575,101 @@ paraminfo_get_equal_hashops(PlannerInfo *root, ParamPathInfo *param_info,
 	return true;
 }
 
+/*
+ * extract_lateral_vars_from_PHVs
+ *	  Extract lateral references within PlaceHolderVars that are due to be
+ *	  evaluated at 'innerrelids'.
+ */
+static List *
+extract_lateral_vars_from_PHVs(PlannerInfo *root, Relids innerrelids)
+{
+	List	   *ph_lateral_vars = NIL;
+	ListCell   *lc;
+
+	/* Nothing would be found if the query contains no LATERAL RTEs */
+	if (!root->hasLateralRTEs)
+		return NIL;
+
+	/*
+	 * No need to consider PHVs that are due to be evaluated at joinrels,
+	 * since we do not add Memoize nodes on top of joinrel paths.
+	 */
+	if (bms_membership(innerrelids) == BMS_MULTIPLE)
+		return NIL;
+
+	foreach(lc, root->placeholder_list)
+	{
+		PlaceHolderInfo *phinfo = (PlaceHolderInfo *) lfirst(lc);
+		List	   *vars;
+		ListCell   *cell;
+
+		/* PHV is uninteresting if no lateral refs */
+		if (phinfo->ph_lateral == NULL)
+			continue;
+
+		/* PHV is uninteresting if not due to be evaluated at innerrelids */
+		if (!bms_equal(phinfo->ph_eval_at, innerrelids))
+			continue;
+
+		/*
+		 * If the PHV does not reference any rels in innerrelids, use its
+		 * contained expression as a cache key rather than extracting the
+		 * Vars/PHVs from it and using those.  This can be beneficial in cases
+		 * where the expression results in fewer distinct values to cache
+		 * tuples for.
+		 */
+		if (!bms_overlap(pull_varnos(root, (Node *) phinfo->ph_var->phexpr),
+						 innerrelids))
+		{
+			ph_lateral_vars = lappend(ph_lateral_vars, phinfo->ph_var->phexpr);
+			continue;
+		}
+
+		/* Fetch Vars and PHVs of lateral references within PlaceHolderVars */
+		vars = pull_vars_of_level((Node *) phinfo->ph_var->phexpr, 0);
+		foreach(cell, vars)
+		{
+			Node	   *node = (Node *) lfirst(cell);
+
+			if (IsA(node, Var))
+			{
+				Var		   *var = (Var *) node;
+
+				Assert(var->varlevelsup == 0);
+
+				if (bms_is_member(var->varno, phinfo->ph_lateral))
+					ph_lateral_vars = lappend(ph_lateral_vars, node);
+			}
+			else if (IsA(node, PlaceHolderVar))
+			{
+				PlaceHolderVar *phv = (PlaceHolderVar *) node;
+
+				Assert(phv->phlevelsup == 0);
+
+				if (bms_is_subset(find_placeholder_info(root, phv)->ph_eval_at,
+								  phinfo->ph_lateral))
+					ph_lateral_vars = lappend(ph_lateral_vars, node);
+			}
+			else
+				Assert(false);
+		}
+
+		list_free(vars);
+	}
+
+	return ph_lateral_vars;
+}
+
 /*
  * get_memoize_path
  *		If possible, make and return a Memoize path atop of 'inner_path'.
  *		Otherwise return NULL.
+ *
+ * Note that currently we do not add Memoize nodes on top of join relation
+ * paths.  This is because the ParamPathInfos for join relation paths do not
+ * maintain ppi_clauses, as the set of relevant clauses varies depending on how
+ * the join is formed.  In addition, joinrels do not maintain lateral_vars.  So
+ * we do not have a way to extract cache keys from joinrels.
  */
 static Path *
 get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel,
@@ -587,6 +681,7 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel,
 	List	   *hash_operators;
 	ListCell   *lc;
 	bool		binary_mode;
+	List	   *ph_lateral_vars;
 
 	/* Obviously not if it's disabled */
 	if (!enable_memoize)
@@ -601,6 +696,13 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel,
 	if (outer_path->parent->rows < 2)
 		return NULL;
 
+	/*
+	 * Extract lateral Vars/PHVs within PlaceHolderVars that are due to be
+	 * evaluated at innerrel.  These lateral Vars/PHVs could be used as
+	 * memoize cache keys.
+	 */
+	ph_lateral_vars = extract_lateral_vars_from_PHVs(root, innerrel->relids);
+
 	/*
 	 * We can only have a memoize node when there's some kind of cache key,
 	 * either parameterized path clauses or lateral Vars.  No cache key sounds
@@ -608,7 +710,8 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel,
 	 */
 	if ((inner_path->param_info == NULL ||
 		 inner_path->param_info->ppi_clauses == NIL) &&
-		innerrel->lateral_vars == NIL)
+		innerrel->lateral_vars == NIL &&
+		ph_lateral_vars == NIL)
 		return NULL;
 
 	/*
@@ -695,6 +798,7 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel,
 									outerrel->top_parent ?
 									outerrel->top_parent : outerrel,
 									innerrel,
+									ph_lateral_vars,
 									&param_exprs,
 									&hash_operators,
 									&binary_mode))
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index 0fd103c06b..96906104d7 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -129,6 +129,99 @@ WHERE t1.unique1 < 10;
     20 | 0.50000000000000000000
 (1 row)
 
+-- Try with LATERAL references within PlaceHolderVars
+SELECT explain_memoize('
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.two+1 AS c1, t2.unique1 AS c2 FROM tenk1 t2) s ON TRUE
+WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
+                                      explain_memoize                                      
+-------------------------------------------------------------------------------------------
+ Aggregate (actual rows=1 loops=N)
+   ->  Nested Loop (actual rows=1000 loops=N)
+         ->  Seq Scan on tenk1 t1 (actual rows=1000 loops=N)
+               Filter: (unique1 < 1000)
+               Rows Removed by Filter: 9000
+         ->  Memoize (actual rows=1 loops=N)
+               Cache Key: (t1.two + 1)
+               Cache Mode: binary
+               Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
+               ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1 loops=N)
+                     Filter: ((t1.two + 1) = unique1)
+                     Rows Removed by Filter: 9999
+                     Heap Fetches: N
+(13 rows)
+
+-- And check we get the expected results.
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.two+1 AS c1, t2.unique1 AS c2 FROM tenk1 t2) s ON TRUE
+WHERE s.c1 = s.c2 AND t1.unique1 < 1000;
+ count |        avg         
+-------+--------------------
+  1000 | 9.5000000000000000
+(1 row)
+
+-- Try with LATERAL references within PlaceHolderVars
+SELECT explain_memoize('
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.two+t2.two AS c1, t2.unique1 AS c2 FROM tenk1 t2) s ON TRUE
+WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
+                                   explain_memoize                                    
+--------------------------------------------------------------------------------------
+ Aggregate (actual rows=1 loops=N)
+   ->  Nested Loop (actual rows=1000 loops=N)
+         ->  Seq Scan on tenk1 t1 (actual rows=1000 loops=N)
+               Filter: (unique1 < 1000)
+               Rows Removed by Filter: 9000
+         ->  Memoize (actual rows=1 loops=N)
+               Cache Key: t1.two
+               Cache Mode: binary
+               Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
+               ->  Seq Scan on tenk1 t2 (actual rows=1 loops=N)
+                     Filter: ((t1.two + two) = unique1)
+                     Rows Removed by Filter: 9999
+(12 rows)
+
+-- And check we get the expected results.
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.two+t2.two AS c1, t2.unique1 AS c2 FROM tenk1 t2) s ON TRUE
+WHERE s.c1 = s.c2 AND t1.unique1 < 1000;
+ count |        avg         
+-------+--------------------
+  1000 | 9.0000000000000000
+(1 row)
+
+-- Ensure we do not omit the cache keys from PlaceHolderVars
+SELECT explain_memoize('
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty AS c1, t2.unique1 AS c2, t2.two FROM tenk1 t2) s
+ON t1.two = s.two
+WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
+                                    explain_memoize                                    
+---------------------------------------------------------------------------------------
+ Aggregate (actual rows=1 loops=N)
+   ->  Nested Loop (actual rows=1000 loops=N)
+         ->  Seq Scan on tenk1 t1 (actual rows=1000 loops=N)
+               Filter: (unique1 < 1000)
+               Rows Removed by Filter: 9000
+         ->  Memoize (actual rows=1 loops=N)
+               Cache Key: t1.two, t1.twenty
+               Cache Mode: binary
+               Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
+               ->  Seq Scan on tenk1 t2 (actual rows=1 loops=N)
+                     Filter: ((t1.twenty = unique1) AND (t1.two = two))
+                     Rows Removed by Filter: 9999
+(12 rows)
+
+-- And check we get the expected results.
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty AS c1, t2.unique1 AS c2, t2.two FROM tenk1 t2) s
+ON t1.two = s.two
+WHERE s.c1 = s.c2 AND t1.unique1 < 1000;
+ count |        avg         
+-------+--------------------
+  1000 | 9.5000000000000000
+(1 row)
+
 SET enable_mergejoin TO off;
 -- Test for varlena datatype with expr evaluation
 CREATE TABLE expr_key (x numeric, t text);
diff --git a/src/test/regress/sql/memoize.sql b/src/test/regress/sql/memoize.sql
index e00e1a94a8..059bec5f4f 100644
--- a/src/test/regress/sql/memoize.sql
+++ b/src/test/regress/sql/memoize.sql
@@ -74,6 +74,41 @@ LATERAL (
 ON t1.two = t2.two
 WHERE t1.unique1 < 10;
 
+-- Try with LATERAL references within PlaceHolderVars
+SELECT explain_memoize('
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.two+1 AS c1, t2.unique1 AS c2 FROM tenk1 t2) s ON TRUE
+WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
+
+-- And check we get the expected results.
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.two+1 AS c1, t2.unique1 AS c2 FROM tenk1 t2) s ON TRUE
+WHERE s.c1 = s.c2 AND t1.unique1 < 1000;
+
+-- Try with LATERAL references within PlaceHolderVars
+SELECT explain_memoize('
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.two+t2.two AS c1, t2.unique1 AS c2 FROM tenk1 t2) s ON TRUE
+WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
+
+-- And check we get the expected results.
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.two+t2.two AS c1, t2.unique1 AS c2 FROM tenk1 t2) s ON TRUE
+WHERE s.c1 = s.c2 AND t1.unique1 < 1000;
+
+-- Ensure we do not omit the cache keys from PlaceHolderVars
+SELECT explain_memoize('
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty AS c1, t2.unique1 AS c2, t2.two FROM tenk1 t2) s
+ON t1.two = s.two
+WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
+
+-- And check we get the expected results.
+SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
+LATERAL (SELECT t1.twenty AS c1, t2.unique1 AS c2, t2.two FROM tenk1 t2) s
+ON t1.two = s.two
+WHERE s.c1 = s.c2 AND t1.unique1 < 1000;
+
 SET enable_mergejoin TO off;
 
 -- Test for varlena datatype with expr evaluation
-- 
2.43.0



^ permalink  raw  reply  [nested|flat] 17+ messages in thread

* Re: Check lateral references within PHVs for memoize cache keys
  2023-12-25 07:01 Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  2024-02-02 09:18 ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  2024-03-18 08:36   ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  2024-06-18 01:47     ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  2024-06-28 14:14       ` Re: Check lateral references within PHVs for memoize cache keys Andrei Lepikhov <[email protected]>
  2024-07-11 09:18         ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
@ 2024-07-12 03:18           ` Andrei Lepikhov <[email protected]>
  1 sibling, 0 replies; 17+ messages in thread

From: Andrei Lepikhov @ 2024-07-12 03:18 UTC (permalink / raw)
  To: Richard Guo <[email protected]>; +Cc: pgsql-hackers; David Rowley <[email protected]>; Tom Lane <[email protected]>

On 7/11/24 16:18, Richard Guo wrote:
> On Fri, Jun 28, 2024 at 10:14 PM Andrei Lepikhov <[email protected]> wrote:
>> I got the point about Memoize over join, but as a join still calls
>> replace_nestloop_params to replace parameters in its clauses, why not to
>> invent something similar to find Memoize keys inside specific JoinPath
>> node? It is not the issue of this patch, though - but is it doable?
> 
> I don't think it's impossible to do, but I'm skeptical that there's an
> easy way to identify all the cache keys for joinrels, without having
> available ppi_clauses and lateral_vars.
Ok

> 
>> IMO, the code:
>> if (bms_nonempty_difference(outerPlan->chgParam, node->keyparamids))
>>     cache_purge_all(node);
>>
>> is a good place to check an assertion: is it really the parent query
>> parameters that make a difference between memoize keys and node list of
>> parameters?
> 
> I don't think we have enough info available here to identify which
> params within outerPlan->chgParam are from outer levels.  Maybe we can
> store root->outer_params in the MemoizeState node to help with this
> assertion, but I'm not sure if it's worth the trouble.
Got it
> 
> Attached is an updated version of this patch.
I'm not sure about stability of output format of AVG aggregate across 
different platforms. Maybe better to return the result of comparison 
between the AVG() and expected value?

-- 
regards, Andrei Lepikhov







^ permalink  raw  reply  [nested|flat] 17+ messages in thread

* Re: Check lateral references within PHVs for memoize cache keys
  2023-12-25 07:01 Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  2024-02-02 09:18 ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  2024-03-18 08:36   ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  2024-06-18 01:47     ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
  2024-06-28 14:14       ` Re: Check lateral references within PHVs for memoize cache keys Andrei Lepikhov <[email protected]>
  2024-07-11 09:18         ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
@ 2024-07-15 01:36           ` Richard Guo <[email protected]>
  1 sibling, 0 replies; 17+ messages in thread

From: Richard Guo @ 2024-07-15 01:36 UTC (permalink / raw)
  To: Andrei Lepikhov <[email protected]>; +Cc: pgsql-hackers; David Rowley <[email protected]>; Tom Lane <[email protected]>

On Thu, Jul 11, 2024 at 5:18 PM Richard Guo <[email protected]> wrote:
> Attached is an updated version of this patch.

I've pushed this patch.  Thanks for all the reviews.

Thanks
Richard






^ permalink  raw  reply  [nested|flat] 17+ messages in thread


end of thread, other threads:[~2024-07-15 01:36 UTC | newest]

Thread overview: 17+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2022-12-10 03:40 [PATCH v6 3/3] Allow recovery via loadable modules. Nathan Bossart <[email protected]>
2022-12-10 03:40 [PATCH v9 1/1] Allow recovery via loadable modules. Nathan Bossart <[email protected]>
2022-12-10 03:40 [PATCH v2 3/3] Allow recovery via loadable modules. Nathan Bossart <[email protected]>
2022-12-10 03:40 [PATCH v3 3/3] Allow recovery via loadable modules. Nathan Bossart <[email protected]>
2022-12-10 03:40 [PATCH v5 3/3] Allow recovery via loadable modules. Nathan Bossart <[email protected]>
2022-12-10 03:40 [PATCH v8 2/2] Allow recovery via loadable modules. Nathan Bossart <[email protected]>
2022-12-10 03:40 [PATCH v7 2/2] Allow recovery via loadable modules. Nathan Bossart <[email protected]>
2022-12-10 03:40 [PATCH v1 3/3] Allow recovery via loadable modules. Nathan Bossart <[email protected]>
2022-12-10 03:40 [PATCH v4 3/3] Allow recovery via loadable modules. Nathan Bossart <[email protected]>
2023-12-25 07:01 Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
2024-02-02 09:18 ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
2024-03-18 08:36   ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
2024-06-18 01:47     ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
2024-06-28 14:14       ` Re: Check lateral references within PHVs for memoize cache keys Andrei Lepikhov <[email protected]>
2024-07-11 09:18         ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>
2024-07-12 03:18           ` Re: Check lateral references within PHVs for memoize cache keys Andrei Lepikhov <[email protected]>
2024-07-15 01:36           ` Re: Check lateral references within PHVs for memoize cache keys Richard Guo <[email protected]>

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox