agora inbox for [email protected]  
help / color / mirror / Atom feed
question on streaming replication
28+ messages / 10 participants
[nested] [flat]

* question on streaming replication
@ 2018-06-14 05:28  Atul Kumar <[email protected]>
  0 siblings, 3 replies; 28+ messages in thread

From: Atul Kumar @ 2018-06-14 05:28 UTC (permalink / raw)
  To: [email protected]; +Cc: pgsql-hackers

Hi,

I have postgres edb 9.6 version, i have below query to solve it out.

i have configured streaming replication having master and slave node
on same  server just to test it.

All worked fine but when i made slave service stop, and create some
test databases in master, after then i made slave service start, slave
didn't pick the changes.

The replication was on async state.

Then after doing some search on google i tried to make it sync state
but even making changes in postgresql.conf file I am neither getting
sync state nor getting any changes on slave server.

Please suggest the needful.


Regards,
Atul




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

* Re: question on streaming replication
@ 2018-06-14 12:14  Fabio Pardi <[email protected]>
  parent: Atul Kumar <[email protected]>
  2 siblings, 0 replies; 28+ messages in thread

From: Fabio Pardi @ 2018-06-14 12:14 UTC (permalink / raw)
  To: [email protected]

Hi Atul,

Please do not cross-post over mailing lists.

As per your problem:

on a streaming replication setup, all changes applied to master are propagated to standby(s).

If standby is stopped or cannot temporary reach master, then it will pick up changes when started or when can reach master again, given that the initial state was in sync with the master.

What stated here above is always true, but there are cases in which too many changes are pushed to master and standby is not able to pick them up.

In order for standby to pick the missing data, is fundamental that the WAL files containing the changes are available to standby or in alternative wal_keep_segments is 'large enough'.

A good starting point to debug the situation are the logfiles of standby server, together with pg_stat_replication table.


Also handy to run on master:

SELECT pg_current_xlog_location() and on standby: select pg_last_xlog_receive_location() to understand if it is picking up. Documentation is always a good starting point to understand what is going on. If you did not already, have a look here: https://www.postgresql.org/docs/9.6/static/runtime-config-replication.html Hope it helps, Fabio


On 14/06/18 07:28, Atul Kumar wrote:
> Hi,
>
> I have postgres edb 9.6 version, i have below query to solve it out.
>
> i have configured streaming replication having master and slave node
> on same  server just to test it.
>
> All worked fine but when i made slave service stop, and create some
> test databases in master, after then i made slave service start, slave
> didn't pick the changes.
>
> The replication was on async state.
>
> Then after doing some search on google i tried to make it sync state
> but even making changes in postgresql.conf file I am neither getting
> sync state nor getting any changes on slave server.
>
> Please suggest the needful.
>
>
> Regards,
> Atul
>





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

* Re: question on streaming replication
@ 2018-06-15 14:36  Amit Kapila <[email protected]>
  parent: Atul Kumar <[email protected]>
  2 siblings, 0 replies; 28+ messages in thread

From: Amit Kapila @ 2018-06-15 14:36 UTC (permalink / raw)
  To: Atul Kumar <[email protected]>; +Cc: [email protected]; pgsql-hackers

On Thu, Jun 14, 2018 at 10:58 AM, Atul Kumar <[email protected]> wrote:
> Hi,
>
> I have postgres edb 9.6 version, i have below query to solve it out.
>

This is not the right place to ask queries on edb versions.  You need
to check with your vendor about the right place to ask questions.
Here, you can ask the questions about PostgreSQL.

> i have configured streaming replication having master and slave node
> on same  server just to test it.
>
> All worked fine but when i made slave service stop, and create some
> test databases in master, after then i made slave service start, slave
> didn't pick the changes.
>

I think you need to ensure that replica is connected to master server
and then probably check logs to confirm what exactly happened.

> The replication was on async state.
>
> Then after doing some search on google i tried to make it sync state
> but even making changes in postgresql.conf file I am neither getting
> sync state nor getting any changes on slave server.
>

After making changes in postgresql.conf, you might need to use
pg_reload_conf  or restart the server depending on the setting you
have changed to make that setting effective.

> Please suggest the needful.
>

As mentioned above, I suggest asking only PostgreSQL related questions
on these mailing lists.

-- 
With Regards,
Amit Kapila.
EnterpriseDB: http://www.enterprisedb.com




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

* Re: question on streaming replication
@ 2018-06-15 16:43  Andreas Kretschmer <[email protected]>
  parent: Atul Kumar <[email protected]>
  2 siblings, 0 replies; 28+ messages in thread

From: Andreas Kretschmer @ 2018-06-15 16:43 UTC (permalink / raw)
  To: [email protected]; Atul Kumar <[email protected]>; [email protected]; +Cc: pgsql-hackers

On 14 June 2018 07:28:53 CEST, Atul Kumar <[email protected]> wrote:
>Hi,
>
>I have postgres edb 9.6 version, i have below query to solve it out.
>
>i have configured streaming replication having master and slave node
>on same  server just to test it.
>
>All worked fine but when i made slave service stop, and create some
>test databases in master, after then i made slave service start, slave
>didn't pick the changes.
>
>The replication was on async state.
>
>Then after doing some search on google i tried to make it sync state
>but even making changes in postgresql.conf file I am neither getting
>sync state nor getting any changes on slave server.
>
>Please suggest the needful.
>
>
>Regards,
>Atul

Sync replication isn't usefull with only one standby. 

I think, during the stop of the standby the master has overwitten needed wal's. You can prevent that by increasing wal_keep_segments or by using replication slots. Please use google, there are tons of docs about that all.


Regards, Andreas


-- 
2ndQuadrant - The PostgreSQL Support Company




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

* Re: Introducing SNI in TLS handshake for SSL connections
@ 2018-12-11 14:52  Pablo Iranzo Gómez <[email protected]>
  0 siblings, 2 replies; 28+ messages in thread

From: Pablo Iranzo Gómez @ 2018-12-11 14:52 UTC (permalink / raw)
  To: pgsql-hackers

Hi,

> On 4/24/17 22:26, Florin Asavoaie wrote:
> > If there's nobody against this, I can try to do the patch myself,
> > doesn't look too difficult (I expect it to simply work by
> > calling SSL_set_tlsext_host_name(SSL_context, PQhost(conn))) somewhere
> > in initialize_SSL in fe-secure-openssl.c.
> 
> I had to look up what SNI is:
> https://en.wikipedia.org/wiki/Server_Name_Indication
> 
> This seems useful.
> 
> If you have a patch, please add it here:
> https://commitfest.postgresql.org/14/
> 
> -- 
> Peter Eisentraut              http://www.2ndQuadrant.com/
> PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

I came to this old thread while trying to figure out on how to setup postgres replication behind OpenShift/Kubernetes behind a route (which only forwards 80 or 443 traffic), but could work if SNI is supported on the client using it.

I haven't found any further follow-up on this, but based on the number of posts and questions on many sites on accessing postgres on OpenShift/Kubernetes it could be something good to have supported.

Any further information or plans?

Thanks,
Pablo


-- 

Pablo Iranzo Gómez ([email protected])          GnuPG: 0x5BD8E1E4
Senior Software Engineer - Solutions Engineering           iranzo @ IRC
RHC{A,SS,DS,VA,E,SA,SP,AOSP}, JBCAA        #110-215-852    RHCA Level V

Blog: https://iranzo.github.io                     https://citellus.org


Attachments:

  [application/pgp-signature] signature.asc (228B, ../../[email protected]/2-signature.asc)
  download

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

* Re: Introducing SNI in TLS handshake for SSL connections
@ 2018-12-11 17:18  Andreas Karlsson <[email protected]>
  parent: Pablo Iranzo Gómez <[email protected]>
  1 sibling, 1 reply; 28+ messages in thread

From: Andreas Karlsson @ 2018-12-11 17:18 UTC (permalink / raw)
  To: Pablo Iranzo Gómez <[email protected]>; pgsql-hackers

On 12/11/18 3:52 PM, Pablo Iranzo Gómez wrote:> I came to this old 
thread while trying to figure out on how to setup
> postgres replication behind OpenShift/Kubernetes behind a route (which 
> only forwards 80 or 443 traffic), but could work if SNI is supported on 
> the client using it.
> 
> I haven't found any further follow-up on this, but based on the number 
> of posts and questions on many sites on accessing postgres on 
> OpenShift/Kubernetes it could be something good to have supported.
> 
> Any further information or plans?

I am pretty sure nobody is working on this.

It seems like it would be easy to implement (basically just call 
SSL_set_tlsext_host_name() with the right hostname) with the only issue 
being that we may need to add a new connection string parameter[1] 
because I doubt all users would want SNI enabled by default since 
PostgreSQL itself cannot do anything useful with the hostname, only some 
kind of TLS proxy can. Hopefully there wont be much bike shedding about 
the new connection parameter. :)

Feel free to write a patch if you have the time and submit it to the 
next commitfest[2] for review.

Notes:

1. List of current options: 
https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS
2. https://wiki.postgresql.org/wiki/CommitFest

Andreas




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

* Re: Introducing SNI in TLS handshake for SSL connections
@ 2018-12-12 20:46  Pablo Iranzo Gómez <[email protected]>
  parent: Andreas Karlsson <[email protected]>
  0 siblings, 0 replies; 28+ messages in thread

From: Pablo Iranzo Gómez @ 2018-12-12 20:46 UTC (permalink / raw)
  To: Andreas Karlsson <[email protected]>; +Cc: pgsql-hackers

+++ Andreas Karlsson [11/12/18 18:18 +0100]:
>On 12/11/18 3:52 PM, Pablo Iranzo Gómez wrote:> I came to this old 
>thread while trying to figure out on how to setup
>>postgres replication behind OpenShift/Kubernetes behind a route 
>>(which only forwards 80 or 443 traffic), but could work if SNI is 
>>supported on the client using it.
>>
>>I haven't found any further follow-up on this, but based on the 
>>number of posts and questions on many sites on accessing postgres on 
>>OpenShift/Kubernetes it could be something good to have supported.
>>
>>Any further information or plans?
>
>I am pretty sure nobody is working on this.
>
>It seems like it would be easy to implement (basically just call 
>SSL_set_tlsext_host_name() with the right hostname) with the only 
>issue being that we may need to add a new connection string 
>parameter[1] because I doubt all users would want SNI enabled by 
>default since PostgreSQL itself cannot do anything useful with the 
>hostname, only some kind of TLS proxy can. Hopefully there wont be 
>much bike shedding about the new connection parameter. :)
>
>Feel free to write a patch if you have the time and submit it to the 
>next commitfest[2] for review.

Unfortunately I do not consider myself a coder, so if there is any way
to 'list' this as a 'nice to have' thing so that someone can take the
task and move it forward.

Thanks,
Pablo

>
>Notes:
>
>1. List of current options: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS
>2. https://wiki.postgresql.org/wiki/CommitFest
>
>Andreas
>

-- 

Pablo Iranzo Gómez ([email protected])          GnuPG: 0x5BD8E1E4
Senior Software Engineer - Solutions Engineering           iranzo @ IRC
RHC{A,SS,DS,VA,E,SA,SP,AOSP}, JBCAA        #110-215-852    RHCA Level V

Blog: https://iranzo.github.io                     https://citellus.org


Attachments:

  [application/pgp-signature] signature.asc (228B, ../../[email protected]/2-signature.asc)
  download

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

* Re: Introducing SNI in TLS handshake for SSL connections
@ 2018-12-13 00:30  Andreas Karlsson <[email protected]>
  parent: Pablo Iranzo Gómez <[email protected]>
  1 sibling, 2 replies; 28+ messages in thread

From: Andreas Karlsson @ 2018-12-13 00:30 UTC (permalink / raw)
  To: Pablo Iranzo Gómez <[email protected]>; pgsql-hackers

On 12/11/18 3:52 PM, Pablo Iranzo Gómez wrote:
> I came to this old thread while trying to figure out on how to setup 
> postgres replication behind OpenShift/Kubernetes behind a route (which 
> only forwards 80 or 443 traffic), but could work if SNI is supported on 
> the client using it.

Hm ... while hacking at a patch for this I gave your specific problem 
some more thought.

I am not familiar with OpenShift or Kubernetes but I want you to be 
aware of that whatever proxy you are going to use will still need to be 
aware of, at least a subset of, the PostgreSQL protocol, since similar 
to SMTP's STARTTLS command the PostgreSQL client will start out using 
the plain text PostgreSQL protocol and then request the server to switch 
over to SSL[1]. So it would be necessary to add support for this to 
whatever proxy you intend to use.

Do you know if adding such custom protocol support is easy to do to the 
proxies you refer to? And do you have any links to documentation for 
these solutions?

Notes

1. https://www.postgresql.org/docs/11/protocol-flow.html#id-1.10.5.7.11

Andreas




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

* Re: Introducing SNI in TLS handshake for SSL connections
@ 2018-12-13 06:43  Pablo Iranzo Gómez <[email protected]>
  parent: Andreas Karlsson <[email protected]>
  1 sibling, 1 reply; 28+ messages in thread

From: Pablo Iranzo Gómez @ 2018-12-13 06:43 UTC (permalink / raw)
  To: Andreas Karlsson <[email protected]>; +Cc: pgsql-hackers

Hi Andreas

+++ Andreas Karlsson [13/12/18 01:30 +0100]:
>On 12/11/18 3:52 PM, Pablo Iranzo Gómez wrote:
>>I came to this old thread while trying to figure out on how to setup 
>>postgres replication behind OpenShift/Kubernetes behind a route 
>>(which only forwards 80 or 443 traffic), but could work if SNI is 
>>supported on the client using it.
>
>Hm ... while hacking at a patch for this I gave your specific problem 
>some more thought.

Thanks for this!

>
>I am not familiar with OpenShift or Kubernetes but I want you to be 
>aware of that whatever proxy you are going to use will still need to 

haproxy is what is used behind, the idea is that haproxy by default when
enabled via a 'route' does allow http or https protocol ONLY, BUT
(https://docs.openshift.com/container-platform/3.9/architecture/networking/routes.html),
routers do support TLS with SNI.

As PSQL by default tries TLS and fallbacks to plain psql protocol the
idea behind is that we tell OpenShift route to be 'Secure' and
'passtrough', in this way, when PSQL does speak to '443' port in the
route that goes to the 'pod' running postgres using TLS and SNI, the
connection goes thru without any special protocol change.

>be aware of, at least a subset of, the PostgreSQL protocol, since 
>similar to SMTP's STARTTLS command the PostgreSQL client will start 
>out using the plain text PostgreSQL protocol and then request the 
>server to switch over to SSL[1]. So it would be necessary to add 
>support for this to whatever proxy you intend to use.
>
>Do you know if adding such custom protocol support is easy to do to 
>the proxies you refer to? And do you have any links to documentation 
>for these solutions?

I found some diagrams and other links to SSL and HAProxy in:

https://www.haproxy.com/fr/blog/enhanced-ssl-load-balancing-with-server-name-indication-sni-tls-exte...

Probably: https://tools.ietf.org/html/rfc6066#page-6

Let me know if this is not helpful, and thanks again for your time on
this.

Pablo


>
>Notes
>
>1. https://www.postgresql.org/docs/11/protocol-flow.html#id-1.10.5.7.11
>
>Andreas

-- 

Pablo Iranzo Gómez ([email protected])          GnuPG: 0x5BD8E1E4
Senior Software Engineer - Solutions Engineering           iranzo @ IRC
RHC{A,SS,DS,VA,E,SA,SP,AOSP}, JBCAA        #110-215-852    RHCA Level V

Blog: https://iranzo.github.io                     https://citellus.org


Attachments:

  [application/pgp-signature] signature.asc (228B, ../../[email protected]/2-signature.asc)
  download

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

* Re: Introducing SNI in TLS handshake for SSL connections
@ 2018-12-13 13:07  Andreas Karlsson <[email protected]>
  parent: Pablo Iranzo Gómez <[email protected]>
  0 siblings, 1 reply; 28+ messages in thread

From: Andreas Karlsson @ 2018-12-13 13:07 UTC (permalink / raw)
  To: Pablo Iranzo Gómez <[email protected]>; +Cc: pgsql-hackers

On 12/13/18 7:43 AM, Pablo Iranzo Gómez wrote:
> haproxy is what is used behind, the idea is that haproxy by default when
> enabled via a 'route' does allow http or https protocol ONLY, BUT
> (https://docs.openshift.com/container-platform/3.9/architecture/networking/routes.html), 
> 
> routers do support TLS with SNI.
> 
> As PSQL by default tries TLS and fallbacks to plain psql protocol the
> idea behind is that we tell OpenShift route to be 'Secure' and
> 'passtrough', in this way, when PSQL does speak to '443' port in the
> route that goes to the 'pod' running postgres using TLS and SNI, the
> connection goes thru without any special protocol change.

Sadly that confirms what I feared. Adding SNI to the PostgreSQL protocol 
wont help with solving your use case because the PostgreSQL protocol has 
its own handshake which happens before the SSL handshake so the session 
will not look like SSL to HA Proxy.

Just like HA Proxy does not support STARTTLS for IMAP[1] I do not think 
that it will ever support SSL for the PostgreSQL protocol, SNI or not.

To solve your use case I recommend using something like stunnel, which 
does support SNI, to wrap the unencrypted PostgreSQL protocol in SSL.

This all makes me very skeptical to if there is any realistic use case 
for adding SNI support to libpq, and just adding SNI support feels like 
adding a trap to people who do not know that they should rather use 
stunnel than PostgreSQL's built-in SSL support of they want to route on 
SNI with HA Proxy.

But I will attach my small patch for this, which I am now opposed to, 
anyway so the code exists if a use case turns up in the future (or if it 
turns out my reasoning above is incorrect).

Notes

1. 
https://www.haproxy.com/documentation/haproxy/deployment-guides/exchange-2010/imap4/

Andreas


Attachments:

  [text/x-patch] ssl-sni-v1.patch (3.5K, ../../[email protected]/2-ssl-sni-v1.patch)
  download | inline diff:
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index d2e5b08541e..528757f775d 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1460,6 +1460,23 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-sslsni" xreflabel="sslsni">
+      <term><literal>sslcompression</literal></term>
+      <listitem>
+       <para>
+        If set to 1, the host name is sent to the server using SSL's
+        <acronym>SNI</acronym> (Server Name Indication) extension.  If set
+        to 0, no <acronym>SNI</acronym> extension will be sent.  The default is
+        0.  This parameter is ignored if a connection without SSL is made.
+       </para>
+
+       <para>
+        The PostgreSQL server ignores the <acronym>SNI</acronym> extension,
+        but it can be used by SSL-aware proxy software.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-sslcert" xreflabel="sslcert">
       <term><literal>sslcert</literal></term>
       <listitem>
@@ -7373,6 +7390,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGSSLSNI</envar></primary>
+      </indexterm>
+      <envar>PGSSLSNI</envar> behaves the same as the <xref
+      linkend="libpq-connect-sslsni"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index bc456fec0c2..4587e5ebb5a 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -278,6 +278,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"SSL-Compression", "", 1,
 	offsetof(struct pg_conn, sslcompression)},
 
+	{"sslsni", "PGSSLSNI", "0", NULL,
+		"SSL-SNI", "", 1,
+	offsetof(struct pg_conn, sslsni)},
+
 	{"sslcert", "PGSSLCERT", NULL, NULL,
 		"SSL-Client-Cert", "", 64,
 	offsetof(struct pg_conn, sslcert)},
@@ -3690,6 +3694,8 @@ freePGconn(PGconn *conn)
 		free(conn->sslcrl);
 	if (conn->sslcompression)
 		free(conn->sslcompression);
+	if (conn->sslsni)
+		free(conn->sslsni);
 	if (conn->requirepeer)
 		free(conn->requirepeer);
 	if (conn->connip)
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index beca3492e8d..fdae2eac74f 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -781,6 +781,7 @@ initialize_SSL(PGconn *conn)
 	char		homedir[MAXPGPATH];
 	char		fnbuf[MAXPGPATH];
 	char		sebuf[PG_STRERROR_R_BUFLEN];
+	char	   *host;
 	bool		have_homedir;
 	bool		have_cert;
 	bool		have_rootcert;
@@ -1183,6 +1184,11 @@ initialize_SSL(PGconn *conn)
 #endif
 #endif
 
+	host = conn->connhost[conn->whichhost].host;
+
+	if (conn->sslsni && conn->sslsni[0] == '1' && host)
+		SSL_set_tlsext_host_name(conn->ssl, host);
+
 	return 0;
 }
 
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 66fd317b949..9f69fbdf5fc 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -353,6 +353,7 @@ struct pg_conn
 									 * retransmits */
 	char	   *sslmode;		/* SSL mode (require,prefer,allow,disable) */
 	char	   *sslcompression; /* SSL compression (0 or 1) */
+	char	   *sslsni;			/* SSL SNI extension (0 or 1) */
 	char	   *sslkey;			/* client key filename */
 	char	   *sslcert;		/* client certificate filename */
 	char	   *sslrootcert;	/* root certificate filename */


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

* Re: Introducing SNI in TLS handshake for SSL connections
@ 2018-12-14 01:30  Chapman Flack <[email protected]>
  parent: Andreas Karlsson <[email protected]>
  0 siblings, 0 replies; 28+ messages in thread

From: Chapman Flack @ 2018-12-14 01:30 UTC (permalink / raw)
  To: Andreas Karlsson <[email protected]>; Pablo Iranzo Gómez <[email protected]>; +Cc: pgsql-hackers

On 12/13/18 08:07, Andreas Karlsson wrote:
> But I will attach my small patch for this, which I am now opposed to, anyway
> so the code exists if a use case turns up in the future (or if it turns out
> my reasoning above is incorrect).

Here's the same patch with one small copy-pasto fixed.

-Chap


Attachments:

  [text/x-patch] ssl-sni-v1a.patch (3.5K, ../../[email protected]/2-ssl-sni-v1a.patch)
  download | inline diff:
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index d2e5b08541e..528757f775d 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1460,6 +1460,23 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-sslsni" xreflabel="sslsni">
+      <term><literal>sslsni</literal></term>
+      <listitem>
+       <para>
+        If set to 1, the host name is sent to the server using SSL's
+        <acronym>SNI</acronym> (Server Name Indication) extension.  If set
+        to 0, no <acronym>SNI</acronym> extension will be sent.  The default is
+        0.  This parameter is ignored if a connection without SSL is made.
+       </para>
+
+       <para>
+        The PostgreSQL server ignores the <acronym>SNI</acronym> extension,
+        but it can be used by SSL-aware proxy software.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-sslcert" xreflabel="sslcert">
       <term><literal>sslcert</literal></term>
       <listitem>
@@ -7373,6 +7390,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGSSLSNI</envar></primary>
+      </indexterm>
+      <envar>PGSSLSNI</envar> behaves the same as the <xref
+      linkend="libpq-connect-sslsni"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index bc456fec0c2..4587e5ebb5a 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -278,6 +278,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"SSL-Compression", "", 1,
 	offsetof(struct pg_conn, sslcompression)},
 
+	{"sslsni", "PGSSLSNI", "0", NULL,
+		"SSL-SNI", "", 1,
+	offsetof(struct pg_conn, sslsni)},
+
 	{"sslcert", "PGSSLCERT", NULL, NULL,
 		"SSL-Client-Cert", "", 64,
 	offsetof(struct pg_conn, sslcert)},
@@ -3690,6 +3694,8 @@ freePGconn(PGconn *conn)
 		free(conn->sslcrl);
 	if (conn->sslcompression)
 		free(conn->sslcompression);
+	if (conn->sslsni)
+		free(conn->sslsni);
 	if (conn->requirepeer)
 		free(conn->requirepeer);
 	if (conn->connip)
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index beca3492e8d..fdae2eac74f 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -781,6 +781,7 @@ initialize_SSL(PGconn *conn)
 	char		homedir[MAXPGPATH];
 	char		fnbuf[MAXPGPATH];
 	char		sebuf[PG_STRERROR_R_BUFLEN];
+	char	   *host;
 	bool		have_homedir;
 	bool		have_cert;
 	bool		have_rootcert;
@@ -1183,6 +1184,11 @@ initialize_SSL(PGconn *conn)
 #endif
 #endif
 
+	host = conn->connhost[conn->whichhost].host;
+
+	if (conn->sslsni && conn->sslsni[0] == '1' && host)
+		SSL_set_tlsext_host_name(conn->ssl, host);
+
 	return 0;
 }
 
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 66fd317b949..9f69fbdf5fc 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -353,6 +353,7 @@ struct pg_conn
 									 * retransmits */
 	char	   *sslmode;		/* SSL mode (require,prefer,allow,disable) */
 	char	   *sslcompression; /* SSL compression (0 or 1) */
+	char	   *sslsni;			/* SSL SNI extension (0 or 1) */
 	char	   *sslkey;			/* client key filename */
 	char	   *sslcert;		/* client certificate filename */
 	char	   *sslrootcert;	/* root certificate filename */


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

* Re: Introducing SNI in TLS handshake for SSL connections
@ 2018-12-14 07:37  Pablo Iranzo Gómez <[email protected]>
  parent: Andreas Karlsson <[email protected]>
  1 sibling, 0 replies; 28+ messages in thread

From: Pablo Iranzo Gómez @ 2018-12-14 07:37 UTC (permalink / raw)
  To: Andreas Karlsson <[email protected]>; +Cc: pgsql-hackers

Hi,

+++ Andreas Karlsson [13/12/18 01:30 +0100]:
>On 12/11/18 3:52 PM, Pablo Iranzo Gómez wrote:
>>I came to this old thread while trying to figure out on how to setup 
>>postgres replication behind OpenShift/Kubernetes behind a route 
>>(which only forwards 80 or 443 traffic), but could work if SNI is 
>>supported on the client using it.
>
>Hm ... while hacking at a patch for this I gave your specific problem 
>some more thought.
>
>I am not familiar with OpenShift or Kubernetes but I want you to be 
>aware of that whatever proxy you are going to use will still need to 
>be aware of, at least a subset of, the PostgreSQL protocol, since 
>similar to SMTP's STARTTLS command the PostgreSQL client will start 
>out using the plain text PostgreSQL protocol and then request the 
>server to switch over to SSL[1]. So it would be necessary to add 
>support for this to whatever proxy you intend to use.
>
>Do you know if adding such custom protocol support is easy to do to 
>the proxies you refer to? And do you have any links to documentation 
>for these solutions?

I saw that they did incorporate some changes like SPDY support and other
http related things.

Let me try to find an answer (now sure how long will it take) and come
back.

I've did some basic search at
https://git.haproxy.org/?p=haproxy.git;a=summary but nothing evident
(for me).

I'll keep you updated.
Pablo


>
>Notes
>
>1. https://www.postgresql.org/docs/11/protocol-flow.html#id-1.10.5.7.11
>
>Andreas

-- 

Pablo Iranzo Gómez ([email protected])          GnuPG: 0x5BD8E1E4
Senior Software Engineer - Solutions Engineering           iranzo @ IRC
RHC{A,SS,DS,VA,E,SA,SP,AOSP}, JBCAA        #110-215-852    RHCA Level V

Blog: https://iranzo.github.io                     https://citellus.org


Attachments:

  [application/pgp-signature] signature.asc (228B, ../../[email protected]/2-signature.asc)
  download

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

* [PATCH v10 4/4] introduce restore_library
@ 2023-01-23 19:47  Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 28+ messages in thread

From: Nathan Bossart @ 2023-01-23 19:47 UTC (permalink / raw)

---
 contrib/basic_wal_module/Makefile             |   2 +
 contrib/basic_wal_module/basic_wal_module.c   |  69 ++++++-
 contrib/basic_wal_module/meson.build          |   5 +
 contrib/basic_wal_module/t/001_restore.pl     |  44 +++++
 doc/src/sgml/backup.sgml                      |  43 ++++-
 doc/src/sgml/basic-wal-module.sgml            |  33 ++--
 doc/src/sgml/config.sgml                      |  53 +++++-
 doc/src/sgml/high-availability.sgml           |  23 ++-
 doc/src/sgml/wal-modules.sgml                 | 171 ++++++++++++++++--
 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/startup.c              |  23 ++-
 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 +
 20 files changed, 604 insertions(+), 75 deletions(-)
 create mode 100644 contrib/basic_wal_module/t/001_restore.pl

diff --git a/contrib/basic_wal_module/Makefile b/contrib/basic_wal_module/Makefile
index 1f88aaf469..b92126ff52 100644
--- a/contrib/basic_wal_module/Makefile
+++ b/contrib/basic_wal_module/Makefile
@@ -9,6 +9,8 @@ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_wal_module/basic_wal_mo
 # 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_wal_module/basic_wal_module.c b/contrib/basic_wal_module/basic_wal_module.c
index 78c36656a8..e17585cd93 100644
--- a/contrib/basic_wal_module/basic_wal_module.c
+++ b/contrib/basic_wal_module/basic_wal_module.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
@@ -58,7 +66,7 @@ void
 _PG_init(void)
 {
 	DefineCustomStringVariable("basic_wal_module.archive_directory",
-							   gettext_noop("Archive file destination directory."),
+							   gettext_noop("Archive file source/destination directory."),
 							   NULL,
 							   &archive_directory,
 							   "",
@@ -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_wal_module", file)));
+
+	if (archive_directory == NULL || archive_directory[0] == '\0')
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("\"basic_wal_module.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_wal_module", file)));
+	return true;
+}
diff --git a/contrib/basic_wal_module/meson.build b/contrib/basic_wal_module/meson.build
index 59939d71c4..fe68a806a9 100644
--- a/contrib/basic_wal_module/meson.build
+++ b/contrib/basic_wal_module/meson.build
@@ -31,4 +31,9 @@ tests += {
     # which typical runningcheck users do not have (e.g. buildfarm clients).
     'runningcheck': false,
   },
+  'tap': {
+    'tests': [
+      't/001_restore.pl',
+    ],
+  },
 }
diff --git a/contrib/basic_wal_module/t/001_restore.pl b/contrib/basic_wal_module/t/001_restore.pl
new file mode 100644
index 0000000000..c9f39ea413
--- /dev/null
+++ b/contrib/basic_wal_module/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_wal_module'");
+$node->append_conf('postgresql.conf', "basic_wal_module.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_wal_module'");
+$restore->append_conf('postgresql.conf', "basic_wal_module.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/backup.sgml b/doc/src/sgml/backup.sgml
index 12d9bb3f81..19f79fd2f2 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="wal-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-wal-module.sgml b/doc/src/sgml/basic-wal-module.sgml
index f972566374..ebb9f0d8c3 100644
--- a/doc/src/sgml/basic-wal-module.sgml
+++ b/doc/src/sgml/basic-wal-module.sgml
@@ -8,17 +8,20 @@
  </indexterm>
 
  <para>
-  <filename>basic_wal_module</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="wal-modules"/>.
+  <filename>basic_wal_module</filename> is an example of a write-ahead log
+  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 write-ahead log modules, see
+  <xref linkend="wal-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-wal-module-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_wal_module.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
-   Server crashes may leave temporary files with the prefix
+   When <filename>basic_wal_module</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 bba24bdcb8..feb4ca34af 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3807,7 +3807,8 @@ include_dir 'conf.d'
      recovery when the end of archived WAL is reached, but will keep trying to
      continue recovery by connecting to the sending server as specified by the
      <varname>primary_conninfo</varname> setting and/or by fetching new WAL
-     segments using <varname>restore_command</varname>.  For this mode, the
+     segments using <varname>restore_command</varname> or
+     <varname>restore_library</varname>.  For this mode, the
      parameters from this section and <xref
      linkend="runtime-config-replication-standby"/> are of interest.
      Parameters from <xref linkend="runtime-config-wal-recovery-target"/> will
@@ -3835,7 +3836,8 @@ include_dir 'conf.d'
       <listitem>
        <para>
         The local shell command to execute to retrieve an archived segment of
-        the WAL file series. This parameter is required for archive recovery,
+        the WAL file series. Either <varname>restore_command</varname> or
+        <xref linkend="guc-restore-library"/> is required for archive recovery,
         but optional for streaming replication.
         Any <literal>%f</literal> in the string is
         replaced by the name of the file to retrieve from the archive,
@@ -3870,7 +3872,42 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
 
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>restore_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-restore-library" xreflabel="restore_library">
+      <term><varname>restore_library</varname> (<type>string</type>)
+      <indexterm>
+        <primary><varname>restore_library</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        The library to use for 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="wal-modules"/>.
+       </para>
+
+       <para>
+        This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
        </para>
       </listitem>
      </varlistentry>
@@ -3915,7 +3952,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
        </para>
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>archive_cleanup_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
        </para>
       </listitem>
      </varlistentry>
@@ -3944,7 +3984,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
        </para>
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>recovery_end_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index f180607528..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/doc/src/sgml/wal-modules.sgml b/doc/src/sgml/wal-modules.sgml
index 451c591e17..479b0a7d14 100644
--- a/doc/src/sgml/wal-modules.sgml
+++ b/doc/src/sgml/wal-modules.sgml
@@ -8,27 +8,33 @@
 
  <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 WAL 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).
+  Write-ahead log 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, write-ahead log
+  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>
@@ -43,7 +49,7 @@
   </indexterm>
 
   <sect2 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>
@@ -70,6 +76,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>
   </sect2>
 
   <sect2 id="archive-module-callbacks">
@@ -136,6 +148,139 @@ typedef bool (*ArchiveFileCB) (const char *file, const char *path);
 
 <programlisting>
 typedef void (*ArchiveShutdownCB) (void);
+</programlisting>
+    </para>
+   </sect3>
+  </sect2>
+ </sect1>
+
+ <sect1 id="recovery-modules">
+  <title>Recovery Modules</title>
+  <indexterm zone="recovery-modules">
+   <primary>Recovery Modules</primary>
+  </indexterm>
+
+  <sect2 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>
+  </sect2>
+
+  <sect2 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>
+
+   <sect3 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>
+   </sect3>
+
+   <sect3 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>
+   </sect3>
+
+   <sect3 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>
+   </sect3>
+
+   <sect3 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>
    </sect3>
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 fb4c860bde..0e6d2d9363 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -4886,15 +4886,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);
 	}
 
 	/*
@@ -7309,14 +7310,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 2a5352f879..04f94e4d53 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/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_tables.c b/src/backend/utils/misc/guc_tables.c
index 4ac808ed22..06d36513aa 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -3787,6 +3787,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 d06074b86f..5641e5a3a8 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -270,6 +270,7 @@
 				# placeholders: %p = path of file to restore
 				#               %f = file name only
 				# e.g. 'cp /mnt/server/archivedir/%f %p'
+#restore_library = ''		# library to use for 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;
 
-- 
2.25.1


--7JfCtLOvnd9MIVvH--





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

* [PATCH v18 5/5] introduce restore_library
@ 2023-02-16 06:06  Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 28+ messages in thread

From: Nathan Bossart @ 2023-02-16 06:06 UTC (permalink / raw)

---
 contrib/basic_archive/Makefile                |   2 +
 contrib/basic_archive/basic_archive.c         | 153 +++++++-
 contrib/basic_archive/meson.build             |   5 +
 contrib/basic_archive/t/001_restore.pl        |  44 +++
 doc/src/sgml/archive-and-restore-modules.sgml | 356 +++++++++++++++++-
 doc/src/sgml/backup.sgml                      |  40 +-
 doc/src/sgml/basic-archive.sgml               |  31 +-
 doc/src/sgml/config.sgml                      |  53 ++-
 doc/src/sgml/high-availability.sgml           |  18 +-
 src/backend/access/transam/xlog.c             |  20 +-
 src/backend/access/transam/xlogarchive.c      |  96 ++++-
 src/backend/access/transam/xlogrecovery.c     |  14 +-
 src/backend/postmaster/checkpointer.c         |  54 +++
 src/backend/postmaster/startup.c              |  44 ++-
 src/backend/restore/shell_restore.c           |  85 ++++-
 src/backend/utils/misc/guc_tables.c           |  11 +
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/restore/restore_module.h          | 143 +++++++
 src/include/restore/shell_restore.h           |  16 +-
 src/tools/pgindent/typedefs.list              |   2 +
 20 files changed, 1104 insertions(+), 84 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl
 create mode 100644 src/include/restore/restore_module.h

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 100ed81f12..e61f7d676f 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -10,6 +10,8 @@ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.c
 # typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
 
+TAP_TESTS = 1
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index 6b102e9072..e0d8dce364 100644
--- a/contrib/basic_archive/basic_archive.c
+++ b/contrib/basic_archive/basic_archive.c
@@ -17,6 +17,11 @@
  * a file is successfully archived and then the system crashes before
  * a durable record of the success has been made.
  *
+ * This file also demonstrates a basic restore library implementation that
+ * is roughly equivalent to the following shell command:
+ *
+ *		cp /path/to/archivedir/%f %p
+ *
  * Copyright (c) 2022-2024, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
@@ -33,6 +38,7 @@
 #include "archive/archive_module.h"
 #include "common/int.h"
 #include "miscadmin.h"
+#include "restore/restore_module.h"
 #include "storage/copydir.h"
 #include "storage/fd.h"
 #include "utils/guc.h"
@@ -54,6 +60,16 @@ static void basic_archive_file_internal(const char *file, const char *path);
 static bool check_archive_directory(char **newval, void **extra, GucSource source);
 static bool compare_files(const char *file1, const char *file2);
 static void basic_archive_shutdown(ArchiveModuleState *state);
+static bool basic_restore_configured(RestoreModuleState *state);
+static bool basic_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool basic_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool basic_restore_file(const char *file, const char *path);
+static bool basic_restore_timeline_history_exists(RestoreModuleState *state,
+												  const char *file,
+												  const char *path);
 
 static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.startup_cb = basic_archive_startup,
@@ -62,6 +78,21 @@ static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.shutdown_cb = basic_archive_shutdown
 };
 
+static const RestoreModuleCallbacks basic_restore_callbacks = {
+	.startup_cb = NULL,
+	.restore_wal_segment_configured_cb = basic_restore_configured,
+	.restore_wal_segment_cb = basic_restore_wal_segment,
+	.restore_timeline_history_configured_cb = basic_restore_configured,
+	.restore_timeline_history_cb = basic_restore_timeline_history,
+	.timeline_history_exists_configured_cb = basic_restore_configured,
+	.timeline_history_exists_cb = basic_restore_timeline_history_exists,
+	.archive_cleanup_configured_cb = NULL,
+	.archive_cleanup_cb = NULL,
+	.recovery_end_configured_cb = NULL,
+	.recovery_end_cb = NULL,
+	.shutdown_cb = NULL
+};
+
 /*
  * _PG_init
  *
@@ -71,7 +102,7 @@ void
 _PG_init(void)
 {
 	DefineCustomStringVariable("basic_archive.archive_directory",
-							   gettext_noop("Archive file destination directory."),
+							   gettext_noop("Archive file source/destination directory."),
 							   NULL,
 							   &archive_directory,
 							   "",
@@ -93,6 +124,17 @@ _PG_archive_module_init(void)
 	return &basic_archive_callbacks;
 }
 
+/*
+ * _PG_restore_module_init
+ *
+ * Returns the module's restore callbacks.
+ */
+const RestoreModuleCallbacks *
+_PG_restore_module_init(void)
+{
+	return &basic_restore_callbacks;
+}
+
 /*
  * basic_archive_startup
  *
@@ -431,3 +473,112 @@ basic_archive_shutdown(ArchiveModuleState *state)
 	pfree(data);
 	state->private_data = NULL;
 }
+
+/*
+ * basic_restore_configured
+ *
+ * Checks that archive_directory is not blank.
+ */
+static bool
+basic_restore_configured(RestoreModuleState *state)
+{
+	return archive_directory != NULL && archive_directory[0] != '\0';
+}
+
+/*
+ * basic_restore_wal_segment
+ *
+ * Retrieves one archived WAL segment.
+ */
+static bool
+basic_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
+						  const char *lastRestartPointFileName)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_timeline_history
+ *
+ * Retrieves one timeline history file.
+ */
+static bool
+basic_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file.
+ */
+static bool
+basic_restore_file(const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive",
+					file)));
+
+	/*
+	 * If the file doesn't exist, return false to indicate that there are no
+	 * more files to restore.
+	 */
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	copy_file(archive_path, path);
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive",
+					file)));
+	return true;
+}
+
+/*
+ * basic_restore_timeline_history_exists
+ *
+ * Check whether a timeline history file is present in the archive directory.
+ */
+static bool
+basic_restore_timeline_history_exists(RestoreModuleState *state,
+									  const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("checking existence of \"%s\" via basic_archive",
+					file)));
+
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	ereport(DEBUG1,
+			(errmsg("verified existence of \"%s\" via basic_archive",
+					file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index cc2f62bf36..8e36fa7ed6 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -31,4 +31,9 @@ tests += {
     # typical installcheck users do not have (e.g. buildfarm clients).
     'runningcheck': false,
   },
+  'tap': {
+    'tests': [
+      't/001_restore.pl',
+    ],
+  },
 }
diff --git a/contrib/basic_archive/t/001_restore.pl b/contrib/basic_archive/t/001_restore.pl
new file mode 100644
index 0000000000..6c01f736cf
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,44 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# start a node
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init(has_archiving => 1, allows_streaming => 1);
+my $archive_dir = $node->archive_dir;
+$archive_dir =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
+$node->append_conf('postgresql.conf', "archive_command = ''");
+$node->append_conf('postgresql.conf', "archive_library = 'basic_archive'");
+$node->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$node->start;
+
+# backup the node
+my $backup = 'backup';
+$node->backup($backup);
+
+# generate some new WAL files
+$node->safe_psql('postgres', "CREATE TABLE test (a INT);");
+$node->safe_psql('postgres', "SELECT pg_switch_wal();");
+$node->safe_psql('postgres', "INSERT INTO test VALUES (1);");
+
+# shut down the node (this should archive all WAL files)
+$node->stop;
+
+# restore from the backup
+my $restore = PostgreSQL::Test::Cluster->new('restore');
+$restore->init_from_backup($node, $backup, has_restoring => 1, standby => 0);
+$restore->append_conf('postgresql.conf', "restore_command = ''");
+$restore->append_conf('postgresql.conf', "restore_library = 'basic_archive'");
+$restore->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$restore->start;
+
+# ensure post-backup WAL is replayed
+my $result = $restore->poll_query_until("postgres", "SELECT count(*) = 1 FROM test;");
+is($result, "1", "check restore content");
+
+done_testing();
diff --git a/doc/src/sgml/archive-and-restore-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml
index a52d15082d..15be548a73 100644
--- a/doc/src/sgml/archive-and-restore-modules.sgml
+++ b/doc/src/sgml/archive-and-restore-modules.sgml
@@ -8,15 +8,17 @@
 
  <para>
   PostgreSQL provides infrastructure to create custom modules for continuous
-  archiving (see <xref linkend="continuous-archiving"/>).  While archiving via
-  a shell command (i.e., <xref linkend="guc-archive-command"/>) is much
-  simpler, a custom archive module will often be considerably more robust and
-  performant.
+  archiving and recovery (see <xref linkend="continuous-archiving"/>).  While a
+  shell command (i.e., <xref linkend="guc-archive-command"/>,
+  <xref linkend="guc-restore-command"/>) is much simpler, a custom module will
+  often be considerably more robust and performant.
  </para>
 
  <para>
   The <filename>contrib/basic_archive</filename> module contains a working
-  example, which demonstrates some useful techniques.
+  example, which demonstrates some useful techniques.  As demonstrated in
+  <filename>basic_archive</filename> a single module may serve as both an
+  archive module and a restore module.
  </para>
 
  <sect1 id="archive-modules">
@@ -178,4 +180,348 @@ typedef void (*ArchiveShutdownCB) (ArchiveModuleState *state);
    </sect3>
   </sect2>
  </sect1>
+
+ <sect1 id="restore-modules">
+  <title>Restore Modules</title>
+  <indexterm zone="restore-modules">
+   <primary>Restore Modules</primary>
+  </indexterm>
+
+  <para>
+   When a custom <xref linkend="guc-restore-library"/> is configured,
+   PostgreSQL will use the module for restore actions.  It is
+   ultimately up to the module to decide how to accomplish each task,
+   but some recommendations are listed at
+   <xref linkend="backup-pitr-recovery"/>.
+  </para>
+
+  <para>
+   Restore modules must at least consist of an initialization function
+   (see <xref linkend="restore-module-init"/>) and the required callbacks (see
+   <xref linkend="restore-module-callbacks"/>).  However, restore modules are
+   also permitted to do much more (e.g., declare GUCs and register background
+   workers).
+  </para>
+
+  <sect2 id="restore-module-init">
+   <title>Initialization Functions</title>
+   <indexterm zone="restore-module-init">
+    <primary>_PG_restore_module_init</primary>
+   </indexterm>
+
+   <para>
+    An restore library is loaded by dynamically loading a shared library with
+    the <xref linkend="guc-restore-library"/>'s name as the library base name.
+    The normal library search path is used to locate the library.  To provide
+    the required restore module callbacks and to indicate that the library is
+    actually a restore module, it needs to provide a function named
+    <function>_PG_restore_module_init</function>.  The result of the function
+    must be a pointer to a struct of type
+    <structname>RestoreModuleCallbacks</structname>, which contains everything
+    that the core code needs to know how to make use of the restore module.
+    The return value needs to be of server lifetime, which is typically
+    achieved by defining it as a <literal>static const</literal> variable in
+    global scope.
+
+<programlisting>
+typedef struct RestoreModuleCallbacks
+{
+    RestoreStartupCB startup_cb;
+    RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+    RestoreWalSegmentCB restore_wal_segment_cb;
+    RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+    RestoreTimelineHistoryCB restore_timeline_history_cb;
+    TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+    TimelineHistoryExistsCB timeline_history_exists_cb;
+    ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+    ArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndConfiguredCB recovery_end_configured_cb;
+    RecoveryEndCB recovery_end_cb;
+    RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+</programlisting>
+
+    The <function>restore_wal_segment_configured_cb</function>,
+    <function>restore_wal_segment_cb</function>,
+    <function>restore_timeline_history_configured_cb</function>,
+    <function>restore_timeline_history_cb</function>,
+    <function>timeline_history_exists_configured_cb</function>, and
+    <function>timeline_history_exists_cb</function> callbacks are required for
+    archive recovery but optional for streaming replication.  The others are
+    always optional.
+   </para>
+  </sect2>
+
+  <sect2 id="restore-module-callbacks">
+   <title>Restore Module Callbacks</title>
+
+   <para>
+    The restore callbacks define the actual behavior of the module.  The server
+    will call them as required to execute restore actions.
+   </para>
+
+   <sect3 id="restore-module-startup">
+    <title>Startup Callback</title>
+
+    <para>
+     The <function>startup_cb</function> callback is called shortly after the
+     module is loaded.  This callback can be used to perform any additional
+     initialization required.  If the restore module has state, it can use
+     <structfield>state->private_data</structfield> to store it.
+
+<programlisting>
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+</programlisting>
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment-configured">
+    <title>Restore WAL Segment Configured Callback</title>
+    <para>
+     The <function>restore_wal_segment_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to retrieve WAL files (e.g., its configuration parameters
+     are set to valid values).  If no
+     <function>restore_wal_segment_configured_cb</function> is defined, the
+     server always assumes the module is not configured to retrieve WAL files.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve WAL files
+     by calling the <function>restore_wal_segment_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_wal_segment_cb</function> callback to retrieve WAL
+     files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment">
+    <title>Restore WAL Segment Callback</title>
+    <para>
+     The <function>restore_wal_segment_cb</function> callback is called to
+     retrieve a single archived segment of the WAL file series for archive
+     recovery or streaming replication.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state, const char *file, const char *path, const char *lastRestartPointFileName);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point.  That is the earliest
+     file that must be kept to allow a restore to be restartable, so this
+     information can be used to truncate the archive to just the minimum
+     required to support restarting from the current restore.
+     <replaceable>lastRestartPointFileName</replaceable> is typically only used
+     by warm-standby configurations (see <xref linkend="warm-standby"/>).  Note
+     that if multiple standby servers are restoring from the same archive
+     directory, you will need to ensure that you do not delete WAL files until
+     they are no longer needed by any of the servers.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history-configured">
+    <title>Restore Timeline History Configured Callback</title>
+    <para>
+     The <function>restore_timeline_history_configured_cb</function> callback
+     is called to determine whether the module is fully configured and ready to
+     accept requests to retrieve timeline history files (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>restore_timeline_history_configured_cb</function> is defined,
+     the server always assumes the module is not configured to retrieve
+     timeline history files.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve timeline
+     history files by calling the
+     <function>restore_timeline_history_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_timeline_history_cb</function> callback to retrieve
+     timeline history files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history">
+    <title>Restore Timeline History Callback</title>
+    <para>
+     The <function>restore_timeline_history_cb</function> callback is called to
+     retrieve a single archived timeline history file for archive recovery or
+     streaming replication.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists-configured">
+    <title>Timeline History Exists Configured Callback</title>
+    <para>
+     The <function>timeline_history_exists_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to determine whether a timeline history file exists in the
+     archives (e.g., its configuration parameters are set to valid values).  If
+     no <function>timeline_history_exists_configured_cb</function> is defined,
+     the server always assumes the module is not configured to check whether
+     certain timeline history files exist.
+
+<programlisting>
+typedef bool (*TimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will check whether
+     certain timeline history files are present in the archives by calling the
+     <function>timeline_history_exists_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>timeline_history_exists_cb</function> callback to check if
+     timeline history files exist in the archives.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists">
+    <title>Timeline History Exists Callback</title>
+    <para>
+     The <function>timeline_history_exists_cb</function> callback is called to
+     check whether a timeline history file is present in the archives.
+
+<programlisting>
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file is
+     present in the archives.  If the file is not available in the archives,
+     the callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the timeline history file.
+    </para>
+
+    <para>
+     Some restore modules might not have a dedicated way to determine whether a
+     timeline history file exists.  For example,
+     <varname>restore_command</varname> only tells the server how to retrieve
+     files.  In this case, the <function>timeline_history_exists_cb</function>
+     callback should copy the file to <replaceable>path</replaceable>, which
+     contains the destination's relative path (including the file name), and
+     the restore module should set
+     <structfield>state->timeline_history_exists_cb_copies</structfield> to
+     <literal>true</literal>.  It is recommended to set this flag in the
+     module's <function>startup_cb</function> callback.  This flag tells the
+     server that it should verify that the file was successfully retrieved
+     after invoking this callback.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup-configured">
+    <title>Archive Cleanup Configured Callback</title>
+    <para>
+     The <function>archive_cleanup_configured_cb</function> callback is called
+     to determine whether the module is fully configured and ready to accept
+     requests to remove old WAL files from the archives (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>archive_cleanup_configured_cb</function> is defined, the server
+     always assumes the module is not configured to remove old WAL files.
+
+<programlisting>
+typedef bool (*ArchiveCleanupConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will remove old WAL
+     files by calling the <function>archive_cleanup_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>archive_cleanup_cb</function> callback to remove old WAL files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup">
+    <title>Archive Cleanup Callback</title>
+    <para>
+     The <function>archive_cleanup_cb</function> callback is called at every
+     restart point by the checkpointer process and is intended to provide a
+     mechanism for cleaning up old archived WAL files that are no longer
+     needed by the standby server.
+
+<programlisting>
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the
+     name of the file that includes the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end-configured">
+    <title>Recovery End Configured Callback</title>
+    <para>
+     The <function>recovery_end_configured_cb</function> callback is called to
+     determine whether the module is fully configured and ready to execute its
+     <function>recovery_end_cb</function> callback once at the end of recovery
+     (e.g., its configuration parameters are set to valid values).  If no
+     <function>recovery_end_configured_cb</function> is defined, the server
+     always assumes the module is not configured to execute its
+     <function>recovery_end_cb</function> callback.
+
+<programlisting>
+typedef bool (*RecoveryEndConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will execute the
+     <function>recovery_end_cb</function> callback once at the end of recovery.
+     If <literal>false</literal> is returned, the server will not use the
+     <function>recovery_end_cb</function> callback at the end of recovery.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end">
+    <title>Recovery End Callback</title>
+    <para>
+     The <function>recovery_end_cb</function> callback is called once at the
+     end of recovery and is intended to provide a mechanism for cleanup
+     following the end of replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-shutdown">
+    <title>Shutdown Callback</title>
+    <para>
+     The <function>shutdown_cb</function> callback is called when a process
+     that has loaded the restore module exits (e.g., after an error) or the
+     value of <xref linkend="guc-restore-library"/> changes.  If no
+     <function>shutdown_cb</function> is defined, no special action is taken in
+     these situations.  If the restore module has state, this callback should
+     free it to avoid leaks.
+
+<programlisting>
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+ </programlisting>
+    </para>
+   </sect3>
+  </sect2>
+ </sect1>
 </chapter>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index b3468eea3c..e1643f92e0 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1261,9 +1261,26 @@ SELECT * FROM pg_backup_stop(wait_for_archive => true);
    <para>
     The key part of all this is to set up a recovery configuration that
     describes how you want to recover and how far the recovery should
-    run.  The one thing that you absolutely must specify is the <varname>restore_command</varname>,
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>,
     which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    WAL file segments.
+   </para>
+
+   <para>
+    Like the <varname>archive_library</varname> parameter,
+    <varname>restore_library</varname> is a shared library.  Since such
+    libraries are written in <literal>C</literal>, creating your own may
+    require considerably more effort than writing a shell command.  However,
+    restore modules can be more performance than restoring via shell, and they
+    will have access to many useful server resources.  For more information
+    about creating a <varname>restore_library</varname>, see
+    <xref linkend="restore-modules"/>.
+   </para>
+
+   <para>
+    Like the <varname>archive_command</varname>,
+    <varname>restore_command</varname> is
     a shell command string.  It can contain <literal>%f</literal>, which is
     replaced by the name of the desired WAL file, and <literal>%p</literal>,
     which is replaced by the path name to copy the WAL file to.
@@ -1282,14 +1299,20 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
    </para>
 
    <para>
-    It is important that the command return nonzero exit status on failure.
-    The command <emphasis>will</emphasis> be called requesting files that are not
-    present in the archive; it must return nonzero when so asked.  This is not
-    an error condition.  An exception is that if the command was terminated by
+    It is important that the <varname>restore_command</varname> return nonzero
+    exit status on failure, or, if you are using a
+    <varname>restore_library</varname>, that the restore callbacks return
+    <literal>false</literal> on failure.  The command or library
+    <emphasis>will</emphasis> be called requesting files that are not
+    present in the archive; it must fail when so asked.  This is not
+    an error condition.  An exception is that if the
+    <varname>restore_command</varname> was terminated by
     a signal (other than <systemitem>SIGTERM</systemitem>, which is used as
     part of a database server shutdown) or an error by the shell (such as
     command not found), then recovery will abort and the server will not start
-    up.
+    up.  Likewise, if the restore callbacks provided by the
+    <varname>restore_library</varname> emit an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1313,7 +1336,8 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
     close as possible given the available WAL segments).  Therefore, a normal
     recovery will end with a <quote>file not found</quote> message, the exact text
     of the error message depending upon your choice of
-    <varname>restore_command</varname>.  You may also see an error message
+    <varname>restore_command</varname> or <varname>restore_library</varname>.
+    You may also see an error message
     at the start of recovery for a file named something like
     <filename>00000001.history</filename>.  This is also normal and does not
     indicate a problem in simple recovery situations; see
diff --git a/doc/src/sgml/basic-archive.sgml b/doc/src/sgml/basic-archive.sgml
index b4d43ced20..f3a5a502bd 100644
--- a/doc/src/sgml/basic-archive.sgml
+++ b/doc/src/sgml/basic-archive.sgml
@@ -8,17 +8,20 @@
  </indexterm>
 
  <para>
-  <filename>basic_archive</filename> is an example of an archive module.  This
-  module copies completed WAL segment files to the specified directory.  This
-  may not be especially useful, but it can serve as a starting point for
-  developing your own archive module.  For more information about archive
-  modules, see <xref linkend="archive-modules"/>.
+  <filename>basic_archive</filename> is an example of an archive and restore
+  module.  This module copies completed WAL segment files to or from the
+  specified directory.  This may not be especially useful, but it can serve as
+  a starting point for developing your own archive and restore modules.  For
+  more information about archive and restore modules, see\
+  <xref linkend="archive-and-restore-modules"/>.
  </para>
 
  <para>
-  In order to function, this module must be loaded via
+  For use as an archive module, this module must be loaded via
   <xref linkend="guc-archive-library"/>, and <xref linkend="guc-archive-mode"/>
-  must be enabled.
+  must be enabled.  For use as a restore module, this module must be loaded
+  via <xref linkend="guc-restore-library"/>, and recovery must be enabled (see
+  <xref linkend="runtime-config-wal-archive-recovery"/>).
  </para>
 
  <sect2 id="basic-archive-configuration-parameters">
@@ -34,11 +37,12 @@
     </term>
     <listitem>
      <para>
-      The directory where the server should copy WAL segment files.  This
-      directory must already exist.  The default is an empty string, which
-      effectively halts WAL archiving, but if <xref linkend="guc-archive-mode"/>
-      is enabled, the server will accumulate WAL segment files in the
-      expectation that a value will soon be provided.
+      The directory where the server should copy WAL segment files to or from.
+      This directory must already exist.  The default is an empty string,
+      which, when used for archiving, effectively halts WAL archival, but if
+      <xref linkend="guc-archive-mode"/> is enabled, the server will accumulate
+      WAL segment files in the expectation that a value will soon be provided.
+      When an empty string is used for recovery, restore will fail.
      </para>
     </listitem>
    </varlistentry>
@@ -46,7 +50,7 @@
 
   <para>
    These parameters must be set in <filename>postgresql.conf</filename>.
-   Typical usage might be:
+   Typical usage as an archive module might be:
   </para>
 
 <programlisting>
@@ -61,6 +65,7 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
+   When <filename>basic_archive</filename> is used as an archive module,
    Server crashes may leave temporary files with the prefix
    <filename>archtemp</filename> in the archive directory.  It is recommended to
    delete such files before restarting the server after a crash.  It is safe to
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index b38cbd714a..bba79e19ab 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3906,7 +3906,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
@@ -3934,7 +3935,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,
@@ -3969,7 +3971,42 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
 
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>restore_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-restore-library" xreflabel="restore_library">
+      <term><varname>restore_library</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>restore_library</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        The library to use for restore actions, including retrieving archived
+        files and executing tasks at restartpoints and at recovery end.  Either
+        <xref linkend="guc-restore-command"/> or
+        <varname>restore_library</varname> is required for archive recovery,
+        but optional for streaming replication.  If this parameter is set to an
+        empty string (the default), restoring via shell is enabled, and
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, and
+        <varname>recovery_end_command</varname> are used.  If both
+        <varname>restore_library</varname> and any of
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, or
+        <varname>recovery_end_command</varname> are set, an error will be
+        raised.  Otherwise, the specified shared library is used for restoring.
+        For more information, see <xref linkend="restore-modules"/>.
+       </para>
+
+       <para>
+        This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
        </para>
       </listitem>
      </varlistentry>
@@ -4014,7 +4051,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>
@@ -4043,7 +4083,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
        </para>
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>recovery_end_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index 236c0af65f..f811398fa2 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -627,7 +627,8 @@ protocol to make nodes agree on a serializable transactional order.
    <para>
     In standby mode, the server continuously applies WAL received from the
     primary server. The standby server can read WAL from a WAL archive
-    (see <xref linkend="guc-restore-command"/>) or directly from the primary
+    (see <xref linkend="guc-restore-command"/> and
+    <xref linkend="guc-restore-library"/>) or directly from the primary
     over a TCP connection (streaming replication). The standby server will
     also attempt to restore any WAL found in the standby cluster's
     <filename>pg_wal</filename> directory. That typically happens after a server
@@ -638,8 +639,10 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     At startup, the standby begins by restoring all WAL available in the
-    archive location, calling <varname>restore_command</varname>. Once it
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s callbacks.  Once it
     reaches the end of WAL available there and <varname>restore_command</varname>
+    or <varname>restore_library</varname>
     fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
     If that fails, and streaming replication has been configured, the
     standby tries to connect to the primary server and start streaming WAL
@@ -698,7 +701,8 @@ protocol to make nodes agree on a serializable transactional order.
     server (see <xref linkend="backup-pitr-recovery"/>). Create a file
     <link linkend="file-standby-signal"><filename>standby.signal</filename></link><indexterm><primary>standby.signal</primary></indexterm>
     in the standby's cluster data
-    directory. Set <xref linkend="guc-restore-command"/> to a simple command to copy files from
+    directory. Set <xref linkend="guc-restore-command"/> or
+    <xref linkend="guc-restore-library"/> to copy files from
     the WAL archive. If you plan to have multiple standby servers for high
     availability purposes, make sure that <varname>recovery_target_timeline</varname> is set to
     <literal>latest</literal> (the default), to make the standby server follow the timeline change
@@ -707,7 +711,8 @@ protocol to make nodes agree on a serializable transactional order.
 
    <note>
      <para>
-     <xref linkend="guc-restore-command"/> should return immediately
+     <xref linkend="guc-restore-command"/> and
+     <xref linkend="guc-restore-library"/> should return immediately
      if the file does not exist; the server will retry the command again if
      necessary.
     </para>
@@ -731,8 +736,9 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     If you're using a WAL archive, its size can be minimized using the <xref
-    linkend="guc-archive-cleanup-command"/> parameter to remove files that are no
-    longer required by the standby server.
+    linkend="guc-archive-cleanup-command"/> parameter or the
+    <xref linkend="guc-restore-library"/>'s archive cleanup callbacks to remove
+    files that are no longer required by the standby server.
     The <application>pg_archivecleanup</application> utility is designed specifically to
     be used with <varname>archive_cleanup_command</varname> in typical single-standby
     configurations, see <xref linkend="pgarchivecleanup"/>.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index e5d12f1d15..34a21c40a5 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -83,7 +83,7 @@
 #include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
-#include "restore/shell_restore.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
@@ -5178,18 +5178,19 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during
+	 * The callback is provided the archive file cutoff point for use during
 	 * log shipping replication.  All files earlier than this point can be
 	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_recovery_end_configured())
+	if (recovery_end_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_recovery_end(lastRestartPointFname);
+		RestoreCallbacks->recovery_end_cb(restore_module_state,
+										  lastRestartPointFname);
 	}
 
 	/*
@@ -7673,18 +7674,19 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Finally, execute archive-cleanup callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during
+	 * The callback is provided the archive file cutoff point for use during
 	 * log shipping replication.  All files earlier than this point can be
 	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_archive_cleanup_configured())
+	if (archive_cleanup_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_archive_cleanup(lastRestartPointFname);
+		RestoreCallbacks->archive_cleanup_cb(restore_module_state,
+											 lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index 320fa857ea..8ebbd6ce50 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -23,14 +23,21 @@
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
 #include "common/archive.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/startup.h"
 #include "postmaster/pgarch.h"
 #include "replication/walsender.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
+#include "utils/memutils.h"
+
+char	   *restoreLibrary = "";
+const RestoreModuleCallbacks *RestoreCallbacks = NULL;
+RestoreModuleState *restore_module_state;
 
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
@@ -74,15 +81,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			if (!shell_restore_file_configured())
+			if (!restore_wal_segment_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			if (!shell_restore_file_configured())
+			if (!restore_timeline_history_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			if (!shell_restore_file_configured())
+			if (!timeline_history_exists_configured())
 				goto not_available;
 			break;
 	}
@@ -174,14 +181,17 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			ret = shell_restore_wal_segment(xlogfname, xlogpath,
-											lastRestartPointFname);
+			ret = RestoreCallbacks->restore_wal_segment_cb(restore_module_state,
+														   xlogfname, xlogpath,
+														   lastRestartPointFname);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->restore_timeline_history_cb(restore_module_state,
+																xlogfname, xlogpath);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->timeline_history_exists_cb(restore_module_state,
+															   xlogfname, xlogpath);
 			break;
 	}
 
@@ -189,6 +199,16 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 
 	if (ret)
 	{
+		/*
+		 * Some restore functions might not copy the file (e.g., checking
+		 * whether a timeline history file exists), but they can set a flag to
+		 * tell us if they do.  We only need to verify file existence if this
+		 * flag is enabled.
+		 */
+		if (archive_type == ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS &&
+			!restore_module_state->timeline_history_exists_cb_copies)
+			return true;
+
 		/*
 		 * command apparently succeeded, but let's make sure the file is
 		 * really there now and has the correct size.
@@ -630,3 +650,65 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the restore callbacks into our global RestoreCallbacks.  The
+ * caller is responsible for validating the combination of library/command
+ * parameters that are set (e.g., restore_command and restore_library cannot
+ * both be set).
+ */
+void
+LoadRestoreCallbacks(void)
+{
+	RestoreModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_restore_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RestoreModuleInit)
+			load_external_function(restoreLibrary, "_PG_restore_module_init",
+								   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("restore modules have to define the symbol "
+						"_PG_restore_module_init")));
+
+	RestoreCallbacks = (*init) ();
+
+	/* restore state should be freed before calling this function */
+	Assert(restore_module_state == NULL);
+	restore_module_state = (RestoreModuleState *)
+		MemoryContextAllocZero(TopMemoryContext,
+							   sizeof(RestoreModuleState));
+
+	if (RestoreCallbacks->startup_cb != NULL)
+		RestoreCallbacks->startup_cb(restore_module_state);
+}
+
+/*
+ * Call the shutdown callback of the loaded restore module, if defined.  Also,
+ * free the restore module state if it was allocated.
+ *
+ * Processes that load restore libraries should register this via
+ * before_shmem_exit().
+ */
+void
+call_restore_module_shutdown_cb(int code, Datum arg)
+{
+	if (RestoreCallbacks != NULL &&
+		RestoreCallbacks->shutdown_cb != NULL &&
+		restore_module_state != NULL)
+		RestoreCallbacks->shutdown_cb(restore_module_state);
+
+	if (restore_module_state != NULL)
+	{
+		pfree(restore_module_state);
+		restore_module_state = NULL;
+	}
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 0bfa569502..4afdcc3aad 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -51,6 +51,7 @@
 #include "replication/slot.h"
 #include "replication/slotsync.h"
 #include "replication/walreceiver.h"
+#include "restore/restore_module.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1114,18 +1115,21 @@ validateRecoveryParameters(void)
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			(!restore_wal_segment_configured() ||
+			 !restore_timeline_history_configured() ||
+			 !timeline_history_exists_configured()))
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
+					(errmsg("specified neither primary_conninfo nor restore_command nor a configured restore_library"),
 					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
 	}
 	else
 	{
-		if (recoveryRestoreCommand == NULL ||
-			strcmp(recoveryRestoreCommand, "") == 0)
+		if (!restore_wal_segment_configured() ||
+			!restore_timeline_history_configured() ||
+			!timeline_history_exists_configured())
 			ereport(FATAL,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("must specify restore_command when standby mode is not enabled")));
+					 errmsg("must specify restore_command or a configured restore_library when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 46197d56f8..47993aa35e 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -45,6 +45,7 @@
 #include "postmaster/bgwriter.h"
 #include "postmaster/interrupt.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -224,6 +225,25 @@ CheckpointerMain(void)
 												 ALLOCSET_DEFAULT_SIZES);
 	MemoryContextSwitchTo(checkpointer_context);
 
+	/*
+	 * Load restore_library so that we can use its archive-cleanup callback,
+	 * if one is defined.
+	 *
+	 * We also take this opportunity to set up the before_shmem_exit hook for
+	 * the shutdown callback and to check that only one of restore_library,
+	 * archive_cleanup_command is set.  Note that we emit a WARNING upon
+	 * detecting misconfigured parameters, and we clear the callbacks so that
+	 * the archive-cleanup functionality is skipped.  We judge this
+	 * functionality to not be important enough to require blocking
+	 * checkpoints or shutting down the server when the parameters are
+	 * misconfigured.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+										 archiveCleanupCommand, "archive_cleanup_command",
+										 WARNING))
+		LoadRestoreCallbacks();
+
 	/*
 	 * If an exception is encountered, processing resumes here.
 	 *
@@ -556,6 +576,10 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		bool		archive_cleanup_settings_changed = false;
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -571,6 +595,36 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		/*
+		 * If the archive-cleanup settings have changed, call the currently
+		 * loaded shutdown callback and clear all the restore callbacks.
+		 * There's presently no good way to unload a library besides
+		 * restarting the process, and there should be little harm in leaving
+		 * it around, so we just leave it loaded.
+		 */
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			archive_cleanup_settings_changed = true;
+			call_restore_module_shutdown_cb(0, (Datum) 0);
+			RestoreCallbacks = NULL;
+		}
+
+		/*
+		 * As in CheckpointerMain(), we only emit a WARNING if we detect that
+		 * both restore_library and archive_cleanup_command are set.  We do
+		 * this even if the archive-cleanup settings haven't changed to remind
+		 * the user about the misconfiguration.
+		 */
+		if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											 archiveCleanupCommand, "archive_cleanup_command",
+											 WARNING) &&
+			archive_cleanup_settings_changed)
+			LoadRestoreCallbacks();
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index e391bf6aa6..3cbd382e00 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -25,6 +25,7 @@
 #include "libpq/pqsignal.h"
 #include "miscadmin.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "storage/ipc.h"
 #include "storage/pmsignal.h"
 #include "storage/procsignal.h"
@@ -118,13 +119,17 @@ StartupProcShutdownHandler(SIGNAL_ARGS)
  * Re-read the config file.
  *
  * If one of the critical walreceiver options has changed, flag xlog.c
- * to restart it.
+ * to restart it.  Also, check for invalid combinations of the command/library
+ * parameters and reload the restore callbacks if necessary.
  */
 static void
 StartupRereadConfig(void)
 {
 	char	   *conninfo = pstrdup(PrimaryConnInfo);
 	char	   *slotname = pstrdup(PrimarySlotName);
+	char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+	char	   *prevRestoreCommand = pstrdup(recoveryRestoreCommand);
+	char	   *prevRecoveryEndCommand = pstrdup(recoveryEndCommand);
 	bool		tempSlot = wal_receiver_create_temp_slot;
 	bool		conninfoChanged;
 	bool		slotnameChanged;
@@ -146,6 +151,30 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	/*
+	 * If the restore settings have changed, call the currently loaded
+	 * shutdown callback and load the new callbacks.  There's presently no
+	 * good way to unload a library besides restarting the process, and there
+	 * should be little harm in leaving it around, so we just leave it loaded.
+	 */
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_restore_module_shutdown_cb(0, (Datum) 0);
+		LoadRestoreCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
@@ -255,6 +284,19 @@ StartupProcessMain(void)
 	 */
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	LoadRestoreCallbacks();
+
 	/*
 	 * Do what we came for.
 	 */
diff --git a/src/backend/restore/shell_restore.c b/src/backend/restore/shell_restore.c
index 42291625c1..be25f04044 100644
--- a/src/backend/restore/shell_restore.c
+++ b/src/backend/restore/shell_restore.c
@@ -3,7 +3,8 @@
  * shell_restore.c
  *
  * These restore functions use a user-specified shell command (e.g., the
- * restore_command GUC).
+ * restore_command GUC).  It is used as the default, but other modules may
+ * define their own restore logic.
  *
  * Copyright (c) 2024, PostgreSQL Global Development Group
  *
@@ -22,25 +23,76 @@
 #include "common/archive.h"
 #include "common/percentrepl.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static void shell_restore_startup(RestoreModuleState *state);
+static bool shell_restore_file_configured(RestoreModuleState *state);
 static bool shell_restore_file(const char *file, const char *path,
 							   const char *lastRestartPointFileName);
+static bool shell_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool shell_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool shell_archive_cleanup_configured(RestoreModuleState *state);
+static void shell_archive_cleanup(RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+static bool shell_recovery_end_configured(RestoreModuleState *state);
+static void shell_recovery_end(RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
 static void ExecuteRecoveryCommand(const char *command,
 								   const char *commandName,
 								   bool failOnSignal,
 								   uint32 wait_event_info,
 								   const char *lastRestartPointFileName);
 
+static const RestoreModuleCallbacks shell_restore_callbacks = {
+	.startup_cb = shell_restore_startup,
+	.restore_wal_segment_configured_cb = shell_restore_file_configured,
+	.restore_wal_segment_cb = shell_restore_wal_segment,
+	.restore_timeline_history_configured_cb = shell_restore_file_configured,
+	.restore_timeline_history_cb = shell_restore_timeline_history,
+	.timeline_history_exists_configured_cb = shell_restore_file_configured,
+	.timeline_history_exists_cb = shell_restore_timeline_history,
+	.archive_cleanup_configured_cb = shell_archive_cleanup_configured,
+	.archive_cleanup_cb = shell_archive_cleanup,
+	.recovery_end_configured_cb = shell_recovery_end_configured,
+	.recovery_end_cb = shell_recovery_end,
+	.shutdown_cb = NULL
+};
+
+/*
+ * shell_restore_init
+ *
+ * Returns the callbacks for restoring via shell.
+ */
+const RestoreModuleCallbacks *
+shell_restore_init(void)
+{
+	return &shell_restore_callbacks;
+}
+
+/*
+ * Indicates that our timeline_history_exists_cb only attempts to retrieve the
+ * file, so the caller should verify the file exists afterwards.
+ */
+static void
+shell_restore_startup(RestoreModuleState *state)
+{
+	state->timeline_history_exists_cb_copies = true;
+}
+
 /*
  * Attempt to execute a shell-based restore command to retrieve a WAL segment.
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_wal_segment(const char *file, const char *path,
+static bool
+shell_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
 						  const char *lastRestartPointFileName)
 {
 	return shell_restore_file(file, path, lastRestartPointFileName);
@@ -52,8 +104,9 @@ shell_restore_wal_segment(const char *file, const char *path,
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_timeline_history(const char *file, const char *path)
+static bool
+shell_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
 {
 	char		lastRestartPointFname[MAXPGPATH];
 
@@ -64,8 +117,8 @@ shell_restore_timeline_history(const char *file, const char *path)
 /*
  * Check whether restore_command is supplied.
  */
-bool
-shell_restore_file_configured(void)
+static bool
+shell_restore_file_configured(RestoreModuleState *state)
 {
 	return recoveryRestoreCommand[0] != '\0';
 }
@@ -152,8 +205,8 @@ shell_restore_file(const char *file, const char *path,
 /*
  * Check whether archive_cleanup_command is supplied.
  */
-bool
-shell_archive_cleanup_configured(void)
+static bool
+shell_archive_cleanup_configured(RestoreModuleState *state)
 {
 	return archiveCleanupCommand[0] != '\0';
 }
@@ -161,8 +214,9 @@ shell_archive_cleanup_configured(void)
 /*
  * Attempt to execute a shell-based archive cleanup command.
  */
-void
-shell_archive_cleanup(const char *lastRestartPointFileName)
+static void
+shell_archive_cleanup(RestoreModuleState *state,
+					  const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(archiveCleanupCommand, "archive_cleanup_command",
 						   false, WAIT_EVENT_ARCHIVE_CLEANUP_COMMAND,
@@ -172,8 +226,8 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 /*
  * Check whether recovery_end_command is supplied.
  */
-bool
-shell_recovery_end_configured(void)
+static bool
+shell_recovery_end_configured(RestoreModuleState *state)
 {
 	return recoveryEndCommand[0] != '\0';
 }
@@ -181,8 +235,9 @@ shell_recovery_end_configured(void)
 /*
  * Attempt to execute a shell-based end-of-recovery command.
  */
-void
-shell_recovery_end(const char *lastRestartPointFileName)
+static void
+shell_recovery_end(RestoreModuleState *state,
+				   const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(recoveryEndCommand, "recovery_end_command", true,
 						   WAIT_EVENT_RECOVERY_END_COMMAND,
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 45013582a7..724d268d7b 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -70,6 +70,7 @@
 #include "replication/slot.h"
 #include "replication/slotsync.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -3946,6 +3947,16 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"restore_library", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
+			gettext_noop("Sets the library that will be called for restore actions."),
+			NULL
+		},
+		&restoreLibrary,
+		"",
+		NULL, NULL, NULL
+	},
+
 	{
 		{"archive_cleanup_command", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
 			gettext_noop("Sets the shell command that will be executed at every restart point."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index edcc0282b2..2939042b06 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -284,6 +284,7 @@
 				# placeholders: %p = path of file to restore
 				#               %f = file name only
 				# e.g. 'cp /mnt/server/archivedir/%f %p'
+#restore_library = ''		# library to use for restore actions
 #archive_cleanup_command = ''	# command to execute at every restartpoint
 #recovery_end_command = ''	# command to execute at completion of recovery
 
diff --git a/src/include/restore/restore_module.h b/src/include/restore/restore_module.h
new file mode 100644
index 0000000000..cc148e855a
--- /dev/null
+++ b/src/include/restore/restore_module.h
@@ -0,0 +1,143 @@
+/*-------------------------------------------------------------------------
+ *
+ * restore_module.h
+ *		Exports for restore modules.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * src/include/restore/restore_module.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef _RESTORE_MODULE_H
+#define _RESTORE_MODULE_H
+
+/*
+ * The value of the restore_library GUC.
+ */
+extern PGDLLIMPORT char *restoreLibrary;
+
+typedef struct RestoreModuleState
+{
+	/*
+	 * Private data pointer for use by a restore module.  This can be used to
+	 * store state for the module that will be passed to each of its
+	 * callbacks.
+	 */
+	void	   *private_data;
+
+	/*
+	 * Indicates whether the callback that checks for timeline existence
+	 * merely copies the file.  If set to true, the server will verify that
+	 * the timeline history file exists after the callback returns.
+	 */
+	bool		timeline_history_exists_cb_copies;
+} RestoreModuleState;
+
+/*
+ * Restore module callbacks
+ *
+ * These callback functions should be defined by restore libraries and returned
+ * via _PG_restore_module_init().  For more information about the purpose of
+ * each callback, refer to the restore modules documentation.
+ */
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state,
+									 const char *file, const char *path,
+									 const char *lastRestartPointFileName);
+typedef bool (*RestoreTimelineHistoryConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state,
+										  const char *file, const char *path);
+typedef bool (*TimelineHistoryExistsConfiguredCB) (RestoreModuleState *state);
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state,
+										 const char *file, const char *path);
+typedef bool (*ArchiveCleanupConfiguredCB) (RestoreModuleState *state);
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+typedef bool (*RecoveryEndConfiguredCB) (RestoreModuleState *state);
+typedef void (*RecoveryEndCB) (RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+
+typedef struct RestoreModuleCallbacks
+{
+	RestoreStartupCB startup_cb;
+	RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+	RestoreWalSegmentCB restore_wal_segment_cb;
+	RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+	RestoreTimelineHistoryCB restore_timeline_history_cb;
+	TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+	TimelineHistoryExistsCB timeline_history_exists_cb;
+	ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+	ArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndConfiguredCB recovery_end_configured_cb;
+	RecoveryEndCB recovery_end_cb;
+	RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+
+/*
+ * Type of the shared library symbol _PG_restore_module_init that is looked up
+ * when loading a restore library.
+ */
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+
+extern PGDLLEXPORT const RestoreModuleCallbacks *_PG_restore_module_init(void);
+
+extern void LoadRestoreCallbacks(void);
+extern void call_restore_module_shutdown_cb(int code, Datum arg);
+
+extern const RestoreModuleCallbacks *RestoreCallbacks;
+extern RestoreModuleState *restore_module_state;
+
+/*
+ * The following *_configured() functions are convenient wrappers around the
+ * *_configured_cb() callback functions with additional defensive NULL checks.
+ */
+
+static inline bool
+restore_wal_segment_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->restore_wal_segment_configured_cb != NULL &&
+		RestoreCallbacks->restore_wal_segment_cb != NULL &&
+		RestoreCallbacks->restore_wal_segment_configured_cb(restore_module_state);
+}
+
+static inline bool
+restore_timeline_history_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->restore_timeline_history_configured_cb != NULL &&
+		RestoreCallbacks->restore_timeline_history_cb != NULL &&
+		RestoreCallbacks->restore_timeline_history_configured_cb(restore_module_state);
+}
+
+static inline bool
+timeline_history_exists_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->timeline_history_exists_configured_cb != NULL &&
+		RestoreCallbacks->timeline_history_exists_cb != NULL &&
+		RestoreCallbacks->timeline_history_exists_configured_cb(restore_module_state);
+}
+
+static inline bool
+archive_cleanup_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->archive_cleanup_configured_cb != NULL &&
+		RestoreCallbacks->archive_cleanup_cb != NULL &&
+		RestoreCallbacks->archive_cleanup_configured_cb(restore_module_state);
+}
+
+static inline bool
+recovery_end_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->recovery_end_configured_cb != NULL &&
+		RestoreCallbacks->recovery_end_cb != NULL &&
+		RestoreCallbacks->recovery_end_configured_cb(restore_module_state);
+}
+
+#endif							/* _RESTORE_MODULE_H */
diff --git a/src/include/restore/shell_restore.h b/src/include/restore/shell_restore.h
index 28b5b7bc92..6352623866 100644
--- a/src/include/restore/shell_restore.h
+++ b/src/include/restore/shell_restore.h
@@ -12,15 +12,13 @@
 #ifndef _SHELL_RESTORE_H
 #define _SHELL_RESTORE_H
 
-extern bool shell_restore_file_configured(void);
-extern bool shell_restore_wal_segment(const char *file, const char *path,
-									  const char *lastRestartPointFileName);
-extern bool shell_restore_timeline_history(const char *file, const char *path);
+#include "restore/restore_module.h"
 
-extern bool shell_archive_cleanup_configured(void);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-
-extern bool shell_recovery_end_configured(void);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Since the logic for restoring via shell commands is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern const RestoreModuleCallbacks *shell_restore_init(void);
 
 #endif							/* _SHELL_RESTORE_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3a02310f26..6a05313f6b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2386,6 +2386,8 @@ ResourceOwnerDesc
 ResourceReleaseCallback
 ResourceReleaseCallbackItem
 ResourceReleasePhase
+RestoreModuleCallbacks
+RestoreModuleState
 RestoreOptions
 RestorePass
 RestrictInfo
-- 
2.25.1


--IJpNTDwzlM2Ie8A6--





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

* [PATCH v16 5/5] introduce restore_library
@ 2023-02-16 06:06  Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 28+ messages in thread

From: Nathan Bossart @ 2023-02-16 06:06 UTC (permalink / raw)

---
 contrib/basic_archive/Makefile                |   1 +
 contrib/basic_archive/basic_archive.c         | 153 +++++++-
 contrib/basic_archive/meson.build             |   5 +
 contrib/basic_archive/t/001_restore.pl        |  44 +++
 doc/src/sgml/archive-and-restore-modules.sgml | 356 +++++++++++++++++-
 doc/src/sgml/backup.sgml                      |  40 +-
 doc/src/sgml/basic-archive.sgml               |  31 +-
 doc/src/sgml/config.sgml                      |  53 ++-
 doc/src/sgml/high-availability.sgml           |  18 +-
 src/backend/access/transam/xlog.c             |  20 +-
 src/backend/access/transam/xlogarchive.c      |  96 ++++-
 src/backend/access/transam/xlogrecovery.c     |  14 +-
 src/backend/postmaster/checkpointer.c         |  54 +++
 src/backend/postmaster/startup.c              |  44 ++-
 src/backend/restore/shell_restore.c           |  85 ++++-
 src/backend/utils/misc/guc_tables.c           |  11 +
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/restore/restore_module.h          | 143 +++++++
 src/include/restore/shell_restore.h           |  16 +-
 src/tools/pgindent/typedefs.list              |   2 +
 20 files changed, 1103 insertions(+), 84 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl
 create mode 100644 src/include/restore/restore_module.h

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 55d299d650..d12d45e0d2 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -5,6 +5,7 @@ PGFILEDESC = "basic_archive - basic archive module"
 
 REGRESS = basic_archive
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.conf
+TAP_TESTS = 1
 # Disabled because these tests require "shared_preload_libraries=basic_archive",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index 4d78c31859..862689f981 100644
--- a/contrib/basic_archive/basic_archive.c
+++ b/contrib/basic_archive/basic_archive.c
@@ -17,6 +17,11 @@
  * a file is successfully archived and then the system crashes before
  * a durable record of the success has been made.
  *
+ * This file also demonstrates a basic restore library implementation that
+ * is roughly equivalent to the following shell command:
+ *
+ *		cp /path/to/archivedir/%f %p
+ *
  * Copyright (c) 2022-2023, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
@@ -33,6 +38,7 @@
 #include "archive/archive_module.h"
 #include "common/int.h"
 #include "miscadmin.h"
+#include "restore/restore_module.h"
 #include "storage/copydir.h"
 #include "storage/fd.h"
 #include "utils/guc.h"
@@ -54,6 +60,16 @@ static void basic_archive_file_internal(const char *file, const char *path);
 static bool check_archive_directory(char **newval, void **extra, GucSource source);
 static bool compare_files(const char *file1, const char *file2);
 static void basic_archive_shutdown(ArchiveModuleState *state);
+static bool basic_restore_configured(RestoreModuleState *state);
+static bool basic_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool basic_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool basic_restore_file(const char *file, const char *path);
+static bool basic_restore_timeline_history_exists(RestoreModuleState *state,
+												  const char *file,
+												  const char *path);
 
 static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.startup_cb = basic_archive_startup,
@@ -62,6 +78,21 @@ static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.shutdown_cb = basic_archive_shutdown
 };
 
+static const RestoreModuleCallbacks basic_restore_callbacks = {
+	.startup_cb = NULL,
+	.restore_wal_segment_configured_cb = basic_restore_configured,
+	.restore_wal_segment_cb = basic_restore_wal_segment,
+	.restore_timeline_history_configured_cb = basic_restore_configured,
+	.restore_timeline_history_cb = basic_restore_timeline_history,
+	.timeline_history_exists_configured_cb = basic_restore_configured,
+	.timeline_history_exists_cb = basic_restore_timeline_history_exists,
+	.archive_cleanup_configured_cb = NULL,
+	.archive_cleanup_cb = NULL,
+	.recovery_end_configured_cb = NULL,
+	.recovery_end_cb = NULL,
+	.shutdown_cb = NULL
+};
+
 /*
  * _PG_init
  *
@@ -71,7 +102,7 @@ void
 _PG_init(void)
 {
 	DefineCustomStringVariable("basic_archive.archive_directory",
-							   gettext_noop("Archive file destination directory."),
+							   gettext_noop("Archive file source/destination directory."),
 							   NULL,
 							   &archive_directory,
 							   "",
@@ -93,6 +124,17 @@ _PG_archive_module_init(void)
 	return &basic_archive_callbacks;
 }
 
+/*
+ * _PG_restore_module_init
+ *
+ * Returns the module's restore callbacks.
+ */
+const RestoreModuleCallbacks *
+_PG_restore_module_init(void)
+{
+	return &basic_restore_callbacks;
+}
+
 /*
  * basic_archive_startup
  *
@@ -426,3 +468,112 @@ basic_archive_shutdown(ArchiveModuleState *state)
 	pfree(data);
 	state->private_data = NULL;
 }
+
+/*
+ * basic_restore_configured
+ *
+ * Checks that archive_directory is not blank.
+ */
+static bool
+basic_restore_configured(RestoreModuleState *state)
+{
+	return archive_directory != NULL && archive_directory[0] != '\0';
+}
+
+/*
+ * basic_restore_wal_segment
+ *
+ * Retrieves one archived WAL segment.
+ */
+static bool
+basic_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
+						  const char *lastRestartPointFileName)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_timeline_history
+ *
+ * Retrieves one timeline history file.
+ */
+static bool
+basic_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file.
+ */
+static bool
+basic_restore_file(const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive",
+					file)));
+
+	/*
+	 * If the file doesn't exist, return false to indicate that there are no
+	 * more files to restore.
+	 */
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	copy_file(archive_path, path);
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive",
+					file)));
+	return true;
+}
+
+/*
+ * basic_restore_timeline_history_exists
+ *
+ * Check whether a timeline history file is present in the archive directory.
+ */
+static bool
+basic_restore_timeline_history_exists(RestoreModuleState *state,
+									  const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("checking existence of \"%s\" via basic_archive",
+					file)));
+
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	ereport(DEBUG1,
+			(errmsg("verified existence of \"%s\" via basic_archive",
+					file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index bc1380e6f6..4e9f5002de 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -31,4 +31,9 @@ tests += {
     # which typical runningcheck users do not have (e.g. buildfarm clients).
     'runningcheck': false,
   },
+  'tap': {
+    'tests': [
+      't/001_restore.pl',
+    ],
+  },
 }
diff --git a/contrib/basic_archive/t/001_restore.pl b/contrib/basic_archive/t/001_restore.pl
new file mode 100644
index 0000000000..f7fa124695
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,44 @@
+
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# start a node
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init(has_archiving => 1, allows_streaming => 1);
+my $archive_dir = $node->archive_dir;
+$archive_dir =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
+$node->append_conf('postgresql.conf', "archive_command = ''");
+$node->append_conf('postgresql.conf', "archive_library = 'basic_archive'");
+$node->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$node->start;
+
+# backup the node
+my $backup = 'backup';
+$node->backup($backup);
+
+# generate some new WAL files
+$node->safe_psql('postgres', "CREATE TABLE test (a INT);");
+$node->safe_psql('postgres', "SELECT pg_switch_wal();");
+$node->safe_psql('postgres', "INSERT INTO test VALUES (1);");
+
+# shut down the node (this should archive all WAL files)
+$node->stop;
+
+# restore from the backup
+my $restore = PostgreSQL::Test::Cluster->new('restore');
+$restore->init_from_backup($node, $backup, has_restoring => 1, standby => 0);
+$restore->append_conf('postgresql.conf', "restore_command = ''");
+$restore->append_conf('postgresql.conf', "restore_library = 'basic_archive'");
+$restore->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$restore->start;
+
+# ensure post-backup WAL is replayed
+my $result = $restore->poll_query_until("postgres", "SELECT count(*) = 1 FROM test;");
+is($result, "1", "check restore content");
+
+done_testing();
diff --git a/doc/src/sgml/archive-and-restore-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml
index 7ed50a3b52..bf2fdb313a 100644
--- a/doc/src/sgml/archive-and-restore-modules.sgml
+++ b/doc/src/sgml/archive-and-restore-modules.sgml
@@ -8,15 +8,17 @@
 
  <para>
   PostgreSQL provides infrastructure to create custom modules for continuous
-  archiving (see <xref linkend="continuous-archiving"/>).  While archiving via
-  a shell command (i.e., <xref linkend="guc-archive-command"/>) is much
-  simpler, a custom archive module will often be considerably more robust and
-  performant.
+  archiving and recovery (see <xref linkend="continuous-archiving"/>).  While a
+  shell command (i.e., <xref linkend="guc-archive-command"/>,
+  <xref linkend="guc-restore-command"/>) is much simpler, a custom module will
+  often be considerably more robust and performant.
  </para>
 
  <para>
   The <filename>contrib/basic_archive</filename> module contains a working
-  example, which demonstrates some useful techniques.
+  example, which demonstrates some useful techniques.  As demonstrated in
+  <filename>basic_archive</filename> a single module may serve as both an
+  archive module and a restore module.
  </para>
 
  <sect1 id="archive-modules">
@@ -166,4 +168,348 @@ typedef void (*ArchiveShutdownCB) (ArchiveModuleState *state);
    </sect3>
   </sect2>
  </sect1>
+
+ <sect1 id="restore-modules">
+  <title>Restore Modules</title>
+  <indexterm zone="restore-modules">
+   <primary>Restore Modules</primary>
+  </indexterm>
+
+  <para>
+   When a custom <xref linkend="guc-restore-library"/> is configured,
+   PostgreSQL will use the module for restore actions.  It is
+   ultimately up to the module to decide how to accomplish each task,
+   but some recommendations are listed at
+   <xref linkend="backup-pitr-recovery"/>.
+  </para>
+
+  <para>
+   Restore modules must at least consist of an initialization function
+   (see <xref linkend="restore-module-init"/>) and the required callbacks (see
+   <xref linkend="restore-module-callbacks"/>).  However, restore modules are
+   also permitted to do much more (e.g., declare GUCs and register background
+   workers).
+  </para>
+
+  <sect2 id="restore-module-init">
+   <title>Initialization Functions</title>
+   <indexterm zone="restore-module-init">
+    <primary>_PG_restore_module_init</primary>
+   </indexterm>
+
+   <para>
+    An restore library is loaded by dynamically loading a shared library with
+    the <xref linkend="guc-restore-library"/>'s name as the library base name.
+    The normal library search path is used to locate the library.  To provide
+    the required restore module callbacks and to indicate that the library is
+    actually a restore module, it needs to provide a function named
+    <function>_PG_restore_module_init</function>.  The result of the function
+    must be a pointer to a struct of type
+    <structname>RestoreModuleCallbacks</structname>, which contains everything
+    that the core code needs to know how to make use of the restore module.
+    The return value needs to be of server lifetime, which is typically
+    achieved by defining it as a <literal>static const</literal> variable in
+    global scope.
+
+<programlisting>
+typedef struct RestoreModuleCallbacks
+{
+    RestoreStartupCB startup_cb;
+    RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+    RestoreWalSegmentCB restore_wal_segment_cb;
+    RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+    RestoreTimelineHistoryCB restore_timeline_history_cb;
+    TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+    TimelineHistoryExistsCB timeline_history_exists_cb;
+    ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+    ArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndConfiguredCB recovery_end_configured_cb;
+    RecoveryEndCB recovery_end_cb;
+    RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+</programlisting>
+
+    The <function>restore_wal_segment_configured_cb</function>,
+    <function>restore_wal_segment_cb</function>,
+    <function>restore_timeline_history_configured_cb</function>,
+    <function>restore_timeline_history_cb</function>,
+    <function>timeline_history_exists_configured_cb</function>, and
+    <function>timeline_history_exists_cb</function> callbacks are required for
+    archive recovery but optional for streaming replication.  The others are
+    always optional.
+   </para>
+  </sect2>
+
+  <sect2 id="restore-module-callbacks">
+   <title>Restore Module Callbacks</title>
+
+   <para>
+    The restore callbacks define the actual behavior of the module.  The server
+    will call them as required to execute restore actions.
+   </para>
+
+   <sect3 id="restore-module-startup">
+    <title>Startup Callback</title>
+
+    <para>
+     The <function>startup_cb</function> callback is called shortly after the
+     module is loaded.  This callback can be used to perform any additional
+     initialization required.  If the restore module has state, it can use
+     <structfield>state->private_data</structfield> to store it.
+
+<programlisting>
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+</programlisting>
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment-configured">
+    <title>Restore WAL Segment Configured Callback</title>
+    <para>
+     The <function>restore_wal_segment_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to retrieve WAL files (e.g., its configuration parameters
+     are set to valid values).  If no
+     <function>restore_wal_segment_configured_cb</function> is defined, the
+     server always assumes the module is not configured to retrieve WAL files.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve WAL files
+     by calling the <function>restore_wal_segment_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_wal_segment_cb</function> callback to retrieve WAL
+     files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment">
+    <title>Restore WAL Segment Callback</title>
+    <para>
+     The <function>restore_wal_segment_cb</function> callback is called to
+     retrieve a single archived segment of the WAL file series for archive
+     recovery or streaming replication.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state, const char *file, const char *path, const char *lastRestartPointFileName);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point.  That is the earliest
+     file that must be kept to allow a restore to be restartable, so this
+     information can be used to truncate the archive to just the minimum
+     required to support restarting from the current restore.
+     <replaceable>lastRestartPointFileName</replaceable> is typically only used
+     by warm-standby configurations (see <xref linkend="warm-standby"/>).  Note
+     that if multiple standby servers are restoring from the same archive
+     directory, you will need to ensure that you do not delete WAL files until
+     they are no longer needed by any of the servers.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history-configured">
+    <title>Restore Timeline History Configured Callback</title>
+    <para>
+     The <function>restore_timeline_history_configured_cb</function> callback
+     is called to determine whether the module is fully configured and ready to
+     accept requests to retrieve timeline history files (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>restore_timeline_history_configured_cb</function> is defined,
+     the server always assumes the module is not configured to retrieve
+     timeline history files.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve timeline
+     history files by calling the
+     <function>restore_timeline_history_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_timeline_history_cb</function> callback to retrieve
+     timeline history files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history">
+    <title>Restore Timeline History Callback</title>
+    <para>
+     The <function>restore_timeline_history_cb</function> callback is called to
+     retrieve a single archived timeline history file for archive recovery or
+     streaming replication.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists-configured">
+    <title>Timeline History Exists Configured Callback</title>
+    <para>
+     The <function>timeline_history_exists_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to determine whether a timeline history file exists in the
+     archives (e.g., its configuration parameters are set to valid values).  If
+     no <function>timeline_history_exists_configured_cb</function> is defined,
+     the server always assumes the module is not configured to check whether
+     certain timeline history files exist.
+
+<programlisting>
+typedef bool (*TimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will check whether
+     certain timeline history files are present in the archives by calling the
+     <function>timeline_history_exists_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>timeline_history_exists_cb</function> callback to check if
+     timeline history files exist in the archives.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists">
+    <title>Timeline History Exists Callback</title>
+    <para>
+     The <function>timeline_history_exists_cb</function> callback is called to
+     check whether a timeline history file is present in the archives.
+
+<programlisting>
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file is
+     present in the archives.  If the file is not available in the archives,
+     the callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the timeline history file.
+    </para>
+
+    <para>
+     Some restore modules might not have a dedicated way to determine whether a
+     timeline history file exists.  For example,
+     <varname>restore_command</varname> only tells the server how to retrieve
+     files.  In this case, the <function>timeline_history_exists_cb</function>
+     callback should copy the file to <replaceable>path</replaceable>, which
+     contains the destination's relative path (including the file name), and
+     the restore module should set
+     <structfield>state->timeline_history_exists_cb_copies</structfield> to
+     <literal>true</literal>.  It is recommended to set this flag in the
+     module's <function>startup_cb</function> callback.  This flag tells the
+     server that it should verify that the file was successfully retrieved
+     after invoking this callback.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup-configured">
+    <title>Archive Cleanup Configured Callback</title>
+    <para>
+     The <function>archive_cleanup_configured_cb</function> callback is called
+     to determine whether the module is fully configured and ready to accept
+     requests to remove old WAL files from the archives (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>archive_cleanup_configured_cb</function> is defined, the server
+     always assumes the module is not configured to remove old WAL files.
+
+<programlisting>
+typedef bool (*ArchiveCleanupConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will remove old WAL
+     files by calling the <function>archive_cleanup_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>archive_cleanup_cb</function> callback to remove old WAL files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup">
+    <title>Archive Cleanup Callback</title>
+    <para>
+     The <function>archive_cleanup_cb</function> callback is called at every
+     restart point by the checkpointer process and is intended to provide a
+     mechanism for cleaning up old archived WAL files that are no longer
+     needed by the standby server.
+
+<programlisting>
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the
+     name of the file that includes the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end-configured">
+    <title>Recovery End Configured Callback</title>
+    <para>
+     The <function>recovery_end_configured_cb</function> callback is called to
+     determine whether the module is fully configured and ready to execute its
+     <function>recovery_end_cb</function> callback once at the end of recovery
+     (e.g., its configuration parameters are set to valid values).  If no
+     <function>recovery_end_configured_cb</function> is defined, the server
+     always assumes the module is not configured to execute its
+     <function>recovery_end_cb</function> callback.
+
+<programlisting>
+typedef bool (*RecoveryEndConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will execute the
+     <function>recovery_end_cb</function> callback once at the end of recovery.
+     If <literal>false</literal> is returned, the server will not use the
+     <function>recovery_end_cb</function> callback at the end of recovery.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end">
+    <title>Recovery End Callback</title>
+    <para>
+     The <function>recovery_end_cb</function> callback is called once at the
+     end of recovery and is intended to provide a mechanism for cleanup
+     following the end of replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-shutdown">
+    <title>Shutdown Callback</title>
+    <para>
+     The <function>shutdown_cb</function> callback is called when a process
+     that has loaded the restore module exits (e.g., after an error) or the
+     value of <xref linkend="guc-restore-library"/> changes.  If no
+     <function>shutdown_cb</function> is defined, no special action is taken in
+     these situations.  If the restore module has state, this callback should
+     free it to avoid leaks.
+
+<programlisting>
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+ </programlisting>
+    </para>
+   </sect3>
+  </sect2>
+ </sect1>
 </chapter>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index 8cb24d6ae5..4b20651234 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1180,9 +1180,26 @@ SELECT * FROM pg_backup_stop(wait_for_archive => true);
    <para>
     The key part of all this is to set up a recovery configuration that
     describes how you want to recover and how far the recovery should
-    run.  The one thing that you absolutely must specify is the <varname>restore_command</varname>,
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>,
     which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    WAL file segments.
+   </para>
+
+   <para>
+    Like the <varname>archive_library</varname> parameter,
+    <varname>restore_library</varname> is a shared library.  Since such
+    libraries are written in <literal>C</literal>, creating your own may
+    require considerably more effort than writing a shell command.  However,
+    restore modules can be more performance than restoring via shell, and they
+    will have access to many useful server resources.  For more information
+    about creating a <varname>restore_library</varname>, see
+    <xref linkend="restore-modules"/>.
+   </para>
+
+   <para>
+    Like the <varname>archive_command</varname>,
+    <varname>restore_command</varname> is
     a shell command string.  It can contain <literal>%f</literal>, which is
     replaced by the name of the desired WAL file, and <literal>%p</literal>,
     which is replaced by the path name to copy the WAL file to.
@@ -1201,14 +1218,20 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
    </para>
 
    <para>
-    It is important that the command return nonzero exit status on failure.
-    The command <emphasis>will</emphasis> be called requesting files that are not
-    present in the archive; it must return nonzero when so asked.  This is not
-    an error condition.  An exception is that if the command was terminated by
+    It is important that the <varname>restore_command</varname> return nonzero
+    exit status on failure, or, if you are using a
+    <varname>restore_library</varname>, that the restore callbacks return
+    <literal>false</literal> on failure.  The command or library
+    <emphasis>will</emphasis> be called requesting files that are not
+    present in the archive; it must fail when so asked.  This is not
+    an error condition.  An exception is that if the
+    <varname>restore_command</varname> was terminated by
     a signal (other than <systemitem>SIGTERM</systemitem>, which is used as
     part of a database server shutdown) or an error by the shell (such as
     command not found), then recovery will abort and the server will not start
-    up.
+    up.  Likewise, if the restore callbacks provided by the
+    <varname>restore_library</varname> emit an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1232,7 +1255,8 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
     close as possible given the available WAL segments).  Therefore, a normal
     recovery will end with a <quote>file not found</quote> message, the exact text
     of the error message depending upon your choice of
-    <varname>restore_command</varname>.  You may also see an error message
+    <varname>restore_command</varname> or <varname>restore_library</varname>.
+    You may also see an error message
     at the start of recovery for a file named something like
     <filename>00000001.history</filename>.  This is also normal and does not
     indicate a problem in simple recovery situations; see
diff --git a/doc/src/sgml/basic-archive.sgml b/doc/src/sgml/basic-archive.sgml
index b4d43ced20..f3a5a502bd 100644
--- a/doc/src/sgml/basic-archive.sgml
+++ b/doc/src/sgml/basic-archive.sgml
@@ -8,17 +8,20 @@
  </indexterm>
 
  <para>
-  <filename>basic_archive</filename> is an example of an archive module.  This
-  module copies completed WAL segment files to the specified directory.  This
-  may not be especially useful, but it can serve as a starting point for
-  developing your own archive module.  For more information about archive
-  modules, see <xref linkend="archive-modules"/>.
+  <filename>basic_archive</filename> is an example of an archive and restore
+  module.  This module copies completed WAL segment files to or from the
+  specified directory.  This may not be especially useful, but it can serve as
+  a starting point for developing your own archive and restore modules.  For
+  more information about archive and restore modules, see\
+  <xref linkend="archive-and-restore-modules"/>.
  </para>
 
  <para>
-  In order to function, this module must be loaded via
+  For use as an archive module, this module must be loaded via
   <xref linkend="guc-archive-library"/>, and <xref linkend="guc-archive-mode"/>
-  must be enabled.
+  must be enabled.  For use as a restore module, this module must be loaded
+  via <xref linkend="guc-restore-library"/>, and recovery must be enabled (see
+  <xref linkend="runtime-config-wal-archive-recovery"/>).
  </para>
 
  <sect2 id="basic-archive-configuration-parameters">
@@ -34,11 +37,12 @@
     </term>
     <listitem>
      <para>
-      The directory where the server should copy WAL segment files.  This
-      directory must already exist.  The default is an empty string, which
-      effectively halts WAL archiving, but if <xref linkend="guc-archive-mode"/>
-      is enabled, the server will accumulate WAL segment files in the
-      expectation that a value will soon be provided.
+      The directory where the server should copy WAL segment files to or from.
+      This directory must already exist.  The default is an empty string,
+      which, when used for archiving, effectively halts WAL archival, but if
+      <xref linkend="guc-archive-mode"/> is enabled, the server will accumulate
+      WAL segment files in the expectation that a value will soon be provided.
+      When an empty string is used for recovery, restore will fail.
      </para>
     </listitem>
    </varlistentry>
@@ -46,7 +50,7 @@
 
   <para>
    These parameters must be set in <filename>postgresql.conf</filename>.
-   Typical usage might be:
+   Typical usage as an archive module might be:
   </para>
 
 <programlisting>
@@ -61,6 +65,7 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
+   When <filename>basic_archive</filename> is used as an archive module,
    Server crashes may leave temporary files with the prefix
    <filename>archtemp</filename> in the archive directory.  It is recommended to
    delete such files before restarting the server after a crash.  It is safe to
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 3839c72c86..cdc2f902e0 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3751,7 +3751,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
@@ -3779,7 +3780,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,
@@ -3814,7 +3816,42 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
 
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>restore_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-restore-library" xreflabel="restore_library">
+      <term><varname>restore_library</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>restore_library</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        The library to use for restore actions, including retrieving archived
+        files and executing tasks at restartpoints and at recovery end.  Either
+        <xref linkend="guc-restore-command"/> or
+        <varname>restore_library</varname> is required for archive recovery,
+        but optional for streaming replication.  If this parameter is set to an
+        empty string (the default), restoring via shell is enabled, and
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, and
+        <varname>recovery_end_command</varname> are used.  If both
+        <varname>restore_library</varname> and any of
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, or
+        <varname>recovery_end_command</varname> are set, an error will be
+        raised.  Otherwise, the specified shared library is used for restoring.
+        For more information, see <xref linkend="restore-modules"/>.
+       </para>
+
+       <para>
+        This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
        </para>
       </listitem>
      </varlistentry>
@@ -3859,7 +3896,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>
@@ -3888,7 +3928,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
        </para>
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>recovery_end_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index 5f9257313a..09705a14a2 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -627,7 +627,8 @@ protocol to make nodes agree on a serializable transactional order.
    <para>
     In standby mode, the server continuously applies WAL received from the
     primary server. The standby server can read WAL from a WAL archive
-    (see <xref linkend="guc-restore-command"/>) or directly from the primary
+    (see <xref linkend="guc-restore-command"/> and
+    <xref linkend="guc-restore-library"/>) or directly from the primary
     over a TCP connection (streaming replication). The standby server will
     also attempt to restore any WAL found in the standby cluster's
     <filename>pg_wal</filename> directory. That typically happens after a server
@@ -638,8 +639,10 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     At startup, the standby begins by restoring all WAL available in the
-    archive location, calling <varname>restore_command</varname>. Once it
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s callbacks.  Once it
     reaches the end of WAL available there and <varname>restore_command</varname>
+    or <varname>restore_library</varname>
     fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
     If that fails, and streaming replication has been configured, the
     standby tries to connect to the primary server and start streaming WAL
@@ -698,7 +701,8 @@ protocol to make nodes agree on a serializable transactional order.
     server (see <xref linkend="backup-pitr-recovery"/>). Create a file
     <link linkend="file-standby-signal"><filename>standby.signal</filename></link><indexterm><primary>standby.signal</primary></indexterm>
     in the standby's cluster data
-    directory. Set <xref linkend="guc-restore-command"/> to a simple command to copy files from
+    directory. Set <xref linkend="guc-restore-command"/> or
+    <xref linkend="guc-restore-library"/> to copy files from
     the WAL archive. If you plan to have multiple standby servers for high
     availability purposes, make sure that <varname>recovery_target_timeline</varname> is set to
     <literal>latest</literal> (the default), to make the standby server follow the timeline change
@@ -707,7 +711,8 @@ protocol to make nodes agree on a serializable transactional order.
 
    <note>
      <para>
-     <xref linkend="guc-restore-command"/> should return immediately
+     <xref linkend="guc-restore-command"/> and
+     <xref linkend="guc-restore-library"/> should return immediately
      if the file does not exist; the server will retry the command again if
      necessary.
     </para>
@@ -731,8 +736,9 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     If you're using a WAL archive, its size can be minimized using the <xref
-    linkend="guc-archive-cleanup-command"/> parameter to remove files that are no
-    longer required by the standby server.
+    linkend="guc-archive-cleanup-command"/> parameter or the
+    <xref linkend="guc-restore-library"/>'s archive cleanup callbacks to remove
+    files that are no longer required by the standby server.
     The <application>pg_archivecleanup</application> utility is designed specifically to
     be used with <varname>archive_cleanup_command</varname> in typical single-standby
     configurations, see <xref linkend="pgarchivecleanup"/>.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 1307748b4e..c79547e403 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -84,7 +84,7 @@
 #include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
-#include "restore/shell_restore.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
@@ -4983,18 +4983,19 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during
+	 * The callback is provided the archive file cutoff point for use during
 	 * log shipping replication.  All files earlier than this point can be
 	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_recovery_end_configured())
+	if (recovery_end_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_recovery_end(lastRestartPointFname);
+		RestoreCallbacks->recovery_end_cb(restore_module_state,
+										  lastRestartPointFname);
 	}
 
 	/*
@@ -7453,18 +7454,19 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Finally, execute archive-cleanup callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during
+	 * The callback is provided the archive file cutoff point for use during
 	 * log shipping replication.  All files earlier than this point can be
 	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_archive_cleanup_configured())
+	if (archive_cleanup_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_archive_cleanup(lastRestartPointFname);
+		RestoreCallbacks->archive_cleanup_cb(restore_module_state,
+											 lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index a925ac22f6..316b866af0 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -23,15 +23,22 @@
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
 #include "common/archive.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/startup.h"
 #include "postmaster/pgarch.h"
 #include "replication/walsender.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/lwlock.h"
+#include "utils/memutils.h"
+
+char	   *restoreLibrary = "";
+const RestoreModuleCallbacks *RestoreCallbacks = NULL;
+RestoreModuleState *restore_module_state;
 
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
@@ -75,15 +82,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			if (!shell_restore_file_configured())
+			if (!restore_wal_segment_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			if (!shell_restore_file_configured())
+			if (!restore_timeline_history_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			if (!shell_restore_file_configured())
+			if (!timeline_history_exists_configured())
 				goto not_available;
 			break;
 	}
@@ -175,14 +182,17 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			ret = shell_restore_wal_segment(xlogfname, xlogpath,
-											lastRestartPointFname);
+			ret = RestoreCallbacks->restore_wal_segment_cb(restore_module_state,
+														   xlogfname, xlogpath,
+														   lastRestartPointFname);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->restore_timeline_history_cb(restore_module_state,
+																xlogfname, xlogpath);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->timeline_history_exists_cb(restore_module_state,
+															   xlogfname, xlogpath);
 			break;
 	}
 
@@ -190,6 +200,16 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 
 	if (ret)
 	{
+		/*
+		 * Some restore functions might not copy the file (e.g., checking
+		 * whether a timeline history file exists), but they can set a flag to
+		 * tell us if they do.  We only need to verify file existence if this
+		 * flag is enabled.
+		 */
+		if (archive_type == ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS &&
+			!restore_module_state->timeline_history_exists_cb_copies)
+			return true;
+
 		/*
 		 * command apparently succeeded, but let's make sure the file is
 		 * really there now and has the correct size.
@@ -631,3 +651,65 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the restore callbacks into our global RestoreCallbacks.  The
+ * caller is responsible for validating the combination of library/command
+ * parameters that are set (e.g., restore_command and restore_library cannot
+ * both be set).
+ */
+void
+LoadRestoreCallbacks(void)
+{
+	RestoreModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_restore_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RestoreModuleInit)
+			load_external_function(restoreLibrary, "_PG_restore_module_init",
+								   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("restore modules have to define the symbol "
+						"_PG_restore_module_init")));
+
+	RestoreCallbacks = (*init) ();
+
+	/* restore state should be freed before calling this function */
+	Assert(restore_module_state == NULL);
+	restore_module_state = (RestoreModuleState *)
+		MemoryContextAllocZero(TopMemoryContext,
+							   sizeof(RestoreModuleState));
+
+	if (RestoreCallbacks->startup_cb != NULL)
+		RestoreCallbacks->startup_cb(restore_module_state);
+}
+
+/*
+ * Call the shutdown callback of the loaded restore module, if defined.  Also,
+ * free the restore module state if it was allocated.
+ *
+ * Processes that load restore libraries should register this via
+ * before_shmem_exit().
+ */
+void
+call_restore_module_shutdown_cb(int code, Datum arg)
+{
+	if (RestoreCallbacks != NULL &&
+		RestoreCallbacks->shutdown_cb != NULL &&
+		restore_module_state != NULL)
+		RestoreCallbacks->shutdown_cb(restore_module_state);
+
+	if (restore_module_state != NULL)
+	{
+		pfree(restore_module_state);
+		restore_module_state = NULL;
+	}
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index d43c229eca..8d231dcc82 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -50,6 +50,7 @@
 #include "postmaster/startup.h"
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
+#include "restore/restore_module.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1077,18 +1078,21 @@ validateRecoveryParameters(void)
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			(!restore_wal_segment_configured() ||
+			 !restore_timeline_history_configured() ||
+			 !timeline_history_exists_configured()))
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
+					(errmsg("specified neither primary_conninfo nor restore_command nor a configured restore_library"),
 					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
 	}
 	else
 	{
-		if (recoveryRestoreCommand == NULL ||
-			strcmp(recoveryRestoreCommand, "") == 0)
+		if (!restore_wal_segment_configured() ||
+			!restore_timeline_history_configured() ||
+			!timeline_history_exists_configured())
 			ereport(FATAL,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("must specify restore_command when standby mode is not enabled")));
+					 errmsg("must specify restore_command or a configured restore_library when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index ace9893d95..4ded7f3ff3 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -45,6 +45,7 @@
 #include "postmaster/bgwriter.h"
 #include "postmaster/interrupt.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -233,6 +234,25 @@ CheckpointerMain(void)
 												 ALLOCSET_DEFAULT_SIZES);
 	MemoryContextSwitchTo(checkpointer_context);
 
+	/*
+	 * Load restore_library so that we can use its archive-cleanup callback,
+	 * if one is defined.
+	 *
+	 * We also take this opportunity to set up the before_shmem_exit hook for
+	 * the shutdown callback and to check that only one of restore_library,
+	 * archive_cleanup_command is set.  Note that we emit a WARNING upon
+	 * detecting misconfigured parameters, and we clear the callbacks so that
+	 * the archive-cleanup functionality is skipped.  We judge this
+	 * functionality to not be important enough to require blocking
+	 * checkpoints or shutting down the server when the parameters are
+	 * misconfigured.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+										 archiveCleanupCommand, "archive_cleanup_command",
+										 WARNING))
+		LoadRestoreCallbacks();
+
 	/*
 	 * If an exception is encountered, processing resumes here.
 	 *
@@ -547,6 +567,10 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		bool		archive_cleanup_settings_changed = false;
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -562,6 +586,36 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		/*
+		 * If the archive-cleanup settings have changed, call the currently
+		 * loaded shutdown callback and clear all the restore callbacks.
+		 * There's presently no good way to unload a library besides
+		 * restarting the process, and there should be little harm in leaving
+		 * it around, so we just leave it loaded.
+		 */
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			archive_cleanup_settings_changed = true;
+			call_restore_module_shutdown_cb(0, (Datum) 0);
+			RestoreCallbacks = NULL;
+		}
+
+		/*
+		 * As in CheckpointerMain(), we only emit a WARNING if we detect that
+		 * both restore_library and archive_cleanup_command are set.  We do
+		 * this even if the archive-cleanup settings haven't changed to remind
+		 * the user about the misconfiguration.
+		 */
+		if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											 archiveCleanupCommand, "archive_cleanup_command",
+											 WARNING) &&
+			archive_cleanup_settings_changed)
+			LoadRestoreCallbacks();
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index adce0ffcef..8ad570c725 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -29,6 +29,7 @@
 #include "pgstat.h"
 #include "postmaster/interrupt.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
 #include "storage/pmsignal.h"
@@ -148,13 +149,17 @@ StartupProcShutdownHandler(SIGNAL_ARGS)
  * Re-read the config file.
  *
  * If one of the critical walreceiver options has changed, flag xlog.c
- * to restart it.
+ * to restart it.  Also, check for invalid combinations of the command/library
+ * parameters and reload the restore callbacks if necessary.
  */
 static void
 StartupRereadConfig(void)
 {
 	char	   *conninfo = pstrdup(PrimaryConnInfo);
 	char	   *slotname = pstrdup(PrimarySlotName);
+	char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+	char	   *prevRestoreCommand = pstrdup(recoveryRestoreCommand);
+	char	   *prevRecoveryEndCommand = pstrdup(recoveryEndCommand);
 	bool		tempSlot = wal_receiver_create_temp_slot;
 	bool		conninfoChanged;
 	bool		slotnameChanged;
@@ -176,6 +181,30 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	/*
+	 * If the restore settings have changed, call the currently loaded
+	 * shutdown callback and load the new callbacks.  There's presently no
+	 * good way to unload a library besides restarting the process, and there
+	 * should be little harm in leaving it around, so we just leave it loaded.
+	 */
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_restore_module_shutdown_cb(0, (Datum) 0);
+		LoadRestoreCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
@@ -285,6 +314,19 @@ StartupProcessMain(void)
 	 */
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	LoadRestoreCallbacks();
+
 	/*
 	 * Do what we came for.
 	 */
diff --git a/src/backend/restore/shell_restore.c b/src/backend/restore/shell_restore.c
index bc1f4a431a..ce77fde66b 100644
--- a/src/backend/restore/shell_restore.c
+++ b/src/backend/restore/shell_restore.c
@@ -3,7 +3,8 @@
  * shell_restore.c
  *
  * These restore functions use a user-specified shell command (e.g., the
- * restore_command GUC).
+ * restore_command GUC).  It is used as the default, but other modules may
+ * define their own restore logic.
  *
  * Copyright (c) 2023, PostgreSQL Global Development Group
  *
@@ -22,25 +23,76 @@
 #include "common/archive.h"
 #include "common/percentrepl.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static void shell_restore_startup(RestoreModuleState *state);
+static bool shell_restore_file_configured(RestoreModuleState *state);
 static bool shell_restore_file(const char *file, const char *path,
 							   const char *lastRestartPointFileName);
+static bool shell_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool shell_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool shell_archive_cleanup_configured(RestoreModuleState *state);
+static void shell_archive_cleanup(RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+static bool shell_recovery_end_configured(RestoreModuleState *state);
+static void shell_recovery_end(RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
 static void ExecuteRecoveryCommand(const char *command,
 								   const char *commandName,
 								   bool failOnSignal,
 								   uint32 wait_event_info,
 								   const char *lastRestartPointFileName);
 
+static const RestoreModuleCallbacks shell_restore_callbacks = {
+	.startup_cb = shell_restore_startup,
+	.restore_wal_segment_configured_cb = shell_restore_file_configured,
+	.restore_wal_segment_cb = shell_restore_wal_segment,
+	.restore_timeline_history_configured_cb = shell_restore_file_configured,
+	.restore_timeline_history_cb = shell_restore_timeline_history,
+	.timeline_history_exists_configured_cb = shell_restore_file_configured,
+	.timeline_history_exists_cb = shell_restore_timeline_history,
+	.archive_cleanup_configured_cb = shell_archive_cleanup_configured,
+	.archive_cleanup_cb = shell_archive_cleanup,
+	.recovery_end_configured_cb = shell_recovery_end_configured,
+	.recovery_end_cb = shell_recovery_end,
+	.shutdown_cb = NULL
+};
+
+/*
+ * shell_restore_init
+ *
+ * Returns the callbacks for restoring via shell.
+ */
+const RestoreModuleCallbacks *
+shell_restore_init(void)
+{
+	return &shell_restore_callbacks;
+}
+
+/*
+ * Indicates that our timeline_history_exists_cb only attempts to retrieve the
+ * file, so the caller should verify the file exists afterwards.
+ */
+static void
+shell_restore_startup(RestoreModuleState *state)
+{
+	state->timeline_history_exists_cb_copies = true;
+}
+
 /*
  * Attempt to execute a shell-based restore command to retrieve a WAL segment.
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_wal_segment(const char *file, const char *path,
+static bool
+shell_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
 						  const char *lastRestartPointFileName)
 {
 	return shell_restore_file(file, path, lastRestartPointFileName);
@@ -52,8 +104,9 @@ shell_restore_wal_segment(const char *file, const char *path,
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_timeline_history(const char *file, const char *path)
+static bool
+shell_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
 {
 	char		lastRestartPointFname[MAXPGPATH];
 
@@ -64,8 +117,8 @@ shell_restore_timeline_history(const char *file, const char *path)
 /*
  * Check whether restore_command is supplied.
  */
-bool
-shell_restore_file_configured(void)
+static bool
+shell_restore_file_configured(RestoreModuleState *state)
 {
 	return recoveryRestoreCommand[0] != '\0';
 }
@@ -152,8 +205,8 @@ shell_restore_file(const char *file, const char *path,
 /*
  * Check whether archive_cleanup_command is supplied.
  */
-bool
-shell_archive_cleanup_configured(void)
+static bool
+shell_archive_cleanup_configured(RestoreModuleState *state)
 {
 	return archiveCleanupCommand[0] != '\0';
 }
@@ -161,8 +214,9 @@ shell_archive_cleanup_configured(void)
 /*
  * Attempt to execute a shell-based archive cleanup command.
  */
-void
-shell_archive_cleanup(const char *lastRestartPointFileName)
+static void
+shell_archive_cleanup(RestoreModuleState *state,
+					  const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(archiveCleanupCommand, "archive_cleanup_command",
 						   false, WAIT_EVENT_ARCHIVE_CLEANUP_COMMAND,
@@ -172,8 +226,8 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 /*
  * Check whether recovery_end_command is supplied.
  */
-bool
-shell_recovery_end_configured(void)
+static bool
+shell_recovery_end_configured(RestoreModuleState *state)
 {
 	return recoveryEndCommand[0] != '\0';
 }
@@ -181,8 +235,9 @@ shell_recovery_end_configured(void)
 /*
  * Attempt to execute a shell-based end-of-recovery command.
  */
-void
-shell_recovery_end(const char *lastRestartPointFileName)
+static void
+shell_recovery_end(RestoreModuleState *state,
+				   const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(recoveryEndCommand, "recovery_end_command", true,
 						   WAIT_EVENT_RECOVERY_END_COMMAND,
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 4c58574166..21e91908aa 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -67,6 +67,7 @@
 #include "replication/logicallauncher.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -3828,6 +3829,16 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"restore_library", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
+			gettext_noop("Sets the library that will be called for restore actions."),
+			NULL
+		},
+		&restoreLibrary,
+		"",
+		NULL, NULL, NULL
+	},
+
 	{
 		{"archive_cleanup_command", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
 			gettext_noop("Sets the shell command that will be executed at every restart point."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index d08d55c3fe..2f9ad7e1bd 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -272,6 +272,7 @@
 				# placeholders: %p = path of file to restore
 				#               %f = file name only
 				# e.g. 'cp /mnt/server/archivedir/%f %p'
+#restore_library = ''		# library to use for restore actions
 #archive_cleanup_command = ''	# command to execute at every restartpoint
 #recovery_end_command = ''	# command to execute at completion of recovery
 
diff --git a/src/include/restore/restore_module.h b/src/include/restore/restore_module.h
new file mode 100644
index 0000000000..b9a5810aa8
--- /dev/null
+++ b/src/include/restore/restore_module.h
@@ -0,0 +1,143 @@
+/*-------------------------------------------------------------------------
+ *
+ * restore_module.h
+ *		Exports for restore modules.
+ *
+ * Copyright (c) 2023, PostgreSQL Global Development Group
+ *
+ * src/include/restore/restore_module.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef _RESTORE_MODULE_H
+#define _RESTORE_MODULE_H
+
+/*
+ * The value of the restore_library GUC.
+ */
+extern PGDLLIMPORT char *restoreLibrary;
+
+typedef struct RestoreModuleState
+{
+	/*
+	 * Private data pointer for use by a restore module.  This can be used to
+	 * store state for the module that will be passed to each of its
+	 * callbacks.
+	 */
+	void	   *private_data;
+
+	/*
+	 * Indicates whether the callback that checks for timeline existence
+	 * merely copies the file.  If set to true, the server will verify that
+	 * the timeline history file exists after the callback returns.
+	 */
+	bool		timeline_history_exists_cb_copies;
+} RestoreModuleState;
+
+/*
+ * Restore module callbacks
+ *
+ * These callback functions should be defined by restore libraries and returned
+ * via _PG_restore_module_init().  For more information about the purpose of
+ * each callback, refer to the restore modules documentation.
+ */
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state,
+									 const char *file, const char *path,
+									 const char *lastRestartPointFileName);
+typedef bool (*RestoreTimelineHistoryConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state,
+										  const char *file, const char *path);
+typedef bool (*TimelineHistoryExistsConfiguredCB) (RestoreModuleState *state);
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state,
+										 const char *file, const char *path);
+typedef bool (*ArchiveCleanupConfiguredCB) (RestoreModuleState *state);
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+typedef bool (*RecoveryEndConfiguredCB) (RestoreModuleState *state);
+typedef void (*RecoveryEndCB) (RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+
+typedef struct RestoreModuleCallbacks
+{
+	RestoreStartupCB startup_cb;
+	RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+	RestoreWalSegmentCB restore_wal_segment_cb;
+	RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+	RestoreTimelineHistoryCB restore_timeline_history_cb;
+	TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+	TimelineHistoryExistsCB timeline_history_exists_cb;
+	ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+	ArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndConfiguredCB recovery_end_configured_cb;
+	RecoveryEndCB recovery_end_cb;
+	RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+
+/*
+ * Type of the shared library symbol _PG_restore_module_init that is looked up
+ * when loading a restore library.
+ */
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+
+extern PGDLLEXPORT const RestoreModuleCallbacks *_PG_restore_module_init(void);
+
+extern void LoadRestoreCallbacks(void);
+extern void call_restore_module_shutdown_cb(int code, Datum arg);
+
+extern const RestoreModuleCallbacks *RestoreCallbacks;
+extern RestoreModuleState *restore_module_state;
+
+/*
+ * The following *_configured() functions are convenient wrappers around the
+ * *_configured_cb() callback functions with additional defensive NULL checks.
+ */
+
+static inline bool
+restore_wal_segment_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->restore_wal_segment_configured_cb != NULL &&
+		RestoreCallbacks->restore_wal_segment_cb != NULL &&
+		RestoreCallbacks->restore_wal_segment_configured_cb(restore_module_state);
+}
+
+static inline bool
+restore_timeline_history_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->restore_timeline_history_configured_cb != NULL &&
+		RestoreCallbacks->restore_timeline_history_cb != NULL &&
+		RestoreCallbacks->restore_timeline_history_configured_cb(restore_module_state);
+}
+
+static inline bool
+timeline_history_exists_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->timeline_history_exists_configured_cb != NULL &&
+		RestoreCallbacks->timeline_history_exists_cb != NULL &&
+		RestoreCallbacks->timeline_history_exists_configured_cb(restore_module_state);
+}
+
+static inline bool
+archive_cleanup_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->archive_cleanup_configured_cb != NULL &&
+		RestoreCallbacks->archive_cleanup_cb != NULL &&
+		RestoreCallbacks->archive_cleanup_configured_cb(restore_module_state);
+}
+
+static inline bool
+recovery_end_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->recovery_end_configured_cb != NULL &&
+		RestoreCallbacks->recovery_end_cb != NULL &&
+		RestoreCallbacks->recovery_end_configured_cb(restore_module_state);
+}
+
+#endif							/* _RESTORE_MODULE_H */
diff --git a/src/include/restore/shell_restore.h b/src/include/restore/shell_restore.h
index 570588e493..1eeae51d81 100644
--- a/src/include/restore/shell_restore.h
+++ b/src/include/restore/shell_restore.h
@@ -12,15 +12,13 @@
 #ifndef _SHELL_RESTORE_H
 #define _SHELL_RESTORE_H
 
-extern bool shell_restore_file_configured(void);
-extern bool shell_restore_wal_segment(const char *file, const char *path,
-									  const char *lastRestartPointFileName);
-extern bool shell_restore_timeline_history(const char *file, const char *path);
+#include "restore/restore_module.h"
 
-extern bool shell_archive_cleanup_configured(void);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-
-extern bool shell_recovery_end_configured(void);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Since the logic for restoring via shell commands is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern const RestoreModuleCallbacks *shell_restore_init(void);
 
 #endif							/* _SHELL_RESTORE_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 16220cfe44..c70e15aba7 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2362,6 +2362,8 @@ ResourceOwner
 ResourceReleaseCallback
 ResourceReleaseCallbackItem
 ResourceReleasePhase
+RestoreModuleCallbacks
+RestoreModuleState
 RestoreOptions
 RestorePass
 RestrictInfo
-- 
2.25.1


--SLDf9lqlvOQaIe6s--





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

* [PATCH v18 5/5] introduce restore_library
@ 2023-02-16 06:06  Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 28+ messages in thread

From: Nathan Bossart @ 2023-02-16 06:06 UTC (permalink / raw)

---
 contrib/basic_archive/Makefile                |   2 +
 contrib/basic_archive/basic_archive.c         | 153 +++++++-
 contrib/basic_archive/meson.build             |   5 +
 contrib/basic_archive/t/001_restore.pl        |  44 +++
 doc/src/sgml/archive-and-restore-modules.sgml | 356 +++++++++++++++++-
 doc/src/sgml/backup.sgml                      |  40 +-
 doc/src/sgml/basic-archive.sgml               |  31 +-
 doc/src/sgml/config.sgml                      |  53 ++-
 doc/src/sgml/high-availability.sgml           |  18 +-
 src/backend/access/transam/xlog.c             |  20 +-
 src/backend/access/transam/xlogarchive.c      |  96 ++++-
 src/backend/access/transam/xlogrecovery.c     |  14 +-
 src/backend/postmaster/checkpointer.c         |  54 +++
 src/backend/postmaster/startup.c              |  44 ++-
 src/backend/restore/shell_restore.c           |  85 ++++-
 src/backend/utils/misc/guc_tables.c           |  11 +
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/restore/restore_module.h          | 143 +++++++
 src/include/restore/shell_restore.h           |  16 +-
 src/tools/pgindent/typedefs.list              |   2 +
 20 files changed, 1104 insertions(+), 84 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl
 create mode 100644 src/include/restore/restore_module.h

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 100ed81f12..e61f7d676f 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -10,6 +10,8 @@ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.c
 # typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
 
+TAP_TESTS = 1
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index 6b102e9072..e0d8dce364 100644
--- a/contrib/basic_archive/basic_archive.c
+++ b/contrib/basic_archive/basic_archive.c
@@ -17,6 +17,11 @@
  * a file is successfully archived and then the system crashes before
  * a durable record of the success has been made.
  *
+ * This file also demonstrates a basic restore library implementation that
+ * is roughly equivalent to the following shell command:
+ *
+ *		cp /path/to/archivedir/%f %p
+ *
  * Copyright (c) 2022-2024, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
@@ -33,6 +38,7 @@
 #include "archive/archive_module.h"
 #include "common/int.h"
 #include "miscadmin.h"
+#include "restore/restore_module.h"
 #include "storage/copydir.h"
 #include "storage/fd.h"
 #include "utils/guc.h"
@@ -54,6 +60,16 @@ static void basic_archive_file_internal(const char *file, const char *path);
 static bool check_archive_directory(char **newval, void **extra, GucSource source);
 static bool compare_files(const char *file1, const char *file2);
 static void basic_archive_shutdown(ArchiveModuleState *state);
+static bool basic_restore_configured(RestoreModuleState *state);
+static bool basic_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool basic_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool basic_restore_file(const char *file, const char *path);
+static bool basic_restore_timeline_history_exists(RestoreModuleState *state,
+												  const char *file,
+												  const char *path);
 
 static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.startup_cb = basic_archive_startup,
@@ -62,6 +78,21 @@ static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.shutdown_cb = basic_archive_shutdown
 };
 
+static const RestoreModuleCallbacks basic_restore_callbacks = {
+	.startup_cb = NULL,
+	.restore_wal_segment_configured_cb = basic_restore_configured,
+	.restore_wal_segment_cb = basic_restore_wal_segment,
+	.restore_timeline_history_configured_cb = basic_restore_configured,
+	.restore_timeline_history_cb = basic_restore_timeline_history,
+	.timeline_history_exists_configured_cb = basic_restore_configured,
+	.timeline_history_exists_cb = basic_restore_timeline_history_exists,
+	.archive_cleanup_configured_cb = NULL,
+	.archive_cleanup_cb = NULL,
+	.recovery_end_configured_cb = NULL,
+	.recovery_end_cb = NULL,
+	.shutdown_cb = NULL
+};
+
 /*
  * _PG_init
  *
@@ -71,7 +102,7 @@ void
 _PG_init(void)
 {
 	DefineCustomStringVariable("basic_archive.archive_directory",
-							   gettext_noop("Archive file destination directory."),
+							   gettext_noop("Archive file source/destination directory."),
 							   NULL,
 							   &archive_directory,
 							   "",
@@ -93,6 +124,17 @@ _PG_archive_module_init(void)
 	return &basic_archive_callbacks;
 }
 
+/*
+ * _PG_restore_module_init
+ *
+ * Returns the module's restore callbacks.
+ */
+const RestoreModuleCallbacks *
+_PG_restore_module_init(void)
+{
+	return &basic_restore_callbacks;
+}
+
 /*
  * basic_archive_startup
  *
@@ -431,3 +473,112 @@ basic_archive_shutdown(ArchiveModuleState *state)
 	pfree(data);
 	state->private_data = NULL;
 }
+
+/*
+ * basic_restore_configured
+ *
+ * Checks that archive_directory is not blank.
+ */
+static bool
+basic_restore_configured(RestoreModuleState *state)
+{
+	return archive_directory != NULL && archive_directory[0] != '\0';
+}
+
+/*
+ * basic_restore_wal_segment
+ *
+ * Retrieves one archived WAL segment.
+ */
+static bool
+basic_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
+						  const char *lastRestartPointFileName)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_timeline_history
+ *
+ * Retrieves one timeline history file.
+ */
+static bool
+basic_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file.
+ */
+static bool
+basic_restore_file(const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive",
+					file)));
+
+	/*
+	 * If the file doesn't exist, return false to indicate that there are no
+	 * more files to restore.
+	 */
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	copy_file(archive_path, path);
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive",
+					file)));
+	return true;
+}
+
+/*
+ * basic_restore_timeline_history_exists
+ *
+ * Check whether a timeline history file is present in the archive directory.
+ */
+static bool
+basic_restore_timeline_history_exists(RestoreModuleState *state,
+									  const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("checking existence of \"%s\" via basic_archive",
+					file)));
+
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	ereport(DEBUG1,
+			(errmsg("verified existence of \"%s\" via basic_archive",
+					file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index cc2f62bf36..8e36fa7ed6 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -31,4 +31,9 @@ tests += {
     # typical installcheck users do not have (e.g. buildfarm clients).
     'runningcheck': false,
   },
+  'tap': {
+    'tests': [
+      't/001_restore.pl',
+    ],
+  },
 }
diff --git a/contrib/basic_archive/t/001_restore.pl b/contrib/basic_archive/t/001_restore.pl
new file mode 100644
index 0000000000..6c01f736cf
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,44 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# start a node
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init(has_archiving => 1, allows_streaming => 1);
+my $archive_dir = $node->archive_dir;
+$archive_dir =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
+$node->append_conf('postgresql.conf', "archive_command = ''");
+$node->append_conf('postgresql.conf', "archive_library = 'basic_archive'");
+$node->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$node->start;
+
+# backup the node
+my $backup = 'backup';
+$node->backup($backup);
+
+# generate some new WAL files
+$node->safe_psql('postgres', "CREATE TABLE test (a INT);");
+$node->safe_psql('postgres', "SELECT pg_switch_wal();");
+$node->safe_psql('postgres', "INSERT INTO test VALUES (1);");
+
+# shut down the node (this should archive all WAL files)
+$node->stop;
+
+# restore from the backup
+my $restore = PostgreSQL::Test::Cluster->new('restore');
+$restore->init_from_backup($node, $backup, has_restoring => 1, standby => 0);
+$restore->append_conf('postgresql.conf', "restore_command = ''");
+$restore->append_conf('postgresql.conf', "restore_library = 'basic_archive'");
+$restore->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$restore->start;
+
+# ensure post-backup WAL is replayed
+my $result = $restore->poll_query_until("postgres", "SELECT count(*) = 1 FROM test;");
+is($result, "1", "check restore content");
+
+done_testing();
diff --git a/doc/src/sgml/archive-and-restore-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml
index a52d15082d..15be548a73 100644
--- a/doc/src/sgml/archive-and-restore-modules.sgml
+++ b/doc/src/sgml/archive-and-restore-modules.sgml
@@ -8,15 +8,17 @@
 
  <para>
   PostgreSQL provides infrastructure to create custom modules for continuous
-  archiving (see <xref linkend="continuous-archiving"/>).  While archiving via
-  a shell command (i.e., <xref linkend="guc-archive-command"/>) is much
-  simpler, a custom archive module will often be considerably more robust and
-  performant.
+  archiving and recovery (see <xref linkend="continuous-archiving"/>).  While a
+  shell command (i.e., <xref linkend="guc-archive-command"/>,
+  <xref linkend="guc-restore-command"/>) is much simpler, a custom module will
+  often be considerably more robust and performant.
  </para>
 
  <para>
   The <filename>contrib/basic_archive</filename> module contains a working
-  example, which demonstrates some useful techniques.
+  example, which demonstrates some useful techniques.  As demonstrated in
+  <filename>basic_archive</filename> a single module may serve as both an
+  archive module and a restore module.
  </para>
 
  <sect1 id="archive-modules">
@@ -178,4 +180,348 @@ typedef void (*ArchiveShutdownCB) (ArchiveModuleState *state);
    </sect3>
   </sect2>
  </sect1>
+
+ <sect1 id="restore-modules">
+  <title>Restore Modules</title>
+  <indexterm zone="restore-modules">
+   <primary>Restore Modules</primary>
+  </indexterm>
+
+  <para>
+   When a custom <xref linkend="guc-restore-library"/> is configured,
+   PostgreSQL will use the module for restore actions.  It is
+   ultimately up to the module to decide how to accomplish each task,
+   but some recommendations are listed at
+   <xref linkend="backup-pitr-recovery"/>.
+  </para>
+
+  <para>
+   Restore modules must at least consist of an initialization function
+   (see <xref linkend="restore-module-init"/>) and the required callbacks (see
+   <xref linkend="restore-module-callbacks"/>).  However, restore modules are
+   also permitted to do much more (e.g., declare GUCs and register background
+   workers).
+  </para>
+
+  <sect2 id="restore-module-init">
+   <title>Initialization Functions</title>
+   <indexterm zone="restore-module-init">
+    <primary>_PG_restore_module_init</primary>
+   </indexterm>
+
+   <para>
+    An restore library is loaded by dynamically loading a shared library with
+    the <xref linkend="guc-restore-library"/>'s name as the library base name.
+    The normal library search path is used to locate the library.  To provide
+    the required restore module callbacks and to indicate that the library is
+    actually a restore module, it needs to provide a function named
+    <function>_PG_restore_module_init</function>.  The result of the function
+    must be a pointer to a struct of type
+    <structname>RestoreModuleCallbacks</structname>, which contains everything
+    that the core code needs to know how to make use of the restore module.
+    The return value needs to be of server lifetime, which is typically
+    achieved by defining it as a <literal>static const</literal> variable in
+    global scope.
+
+<programlisting>
+typedef struct RestoreModuleCallbacks
+{
+    RestoreStartupCB startup_cb;
+    RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+    RestoreWalSegmentCB restore_wal_segment_cb;
+    RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+    RestoreTimelineHistoryCB restore_timeline_history_cb;
+    TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+    TimelineHistoryExistsCB timeline_history_exists_cb;
+    ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+    ArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndConfiguredCB recovery_end_configured_cb;
+    RecoveryEndCB recovery_end_cb;
+    RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+</programlisting>
+
+    The <function>restore_wal_segment_configured_cb</function>,
+    <function>restore_wal_segment_cb</function>,
+    <function>restore_timeline_history_configured_cb</function>,
+    <function>restore_timeline_history_cb</function>,
+    <function>timeline_history_exists_configured_cb</function>, and
+    <function>timeline_history_exists_cb</function> callbacks are required for
+    archive recovery but optional for streaming replication.  The others are
+    always optional.
+   </para>
+  </sect2>
+
+  <sect2 id="restore-module-callbacks">
+   <title>Restore Module Callbacks</title>
+
+   <para>
+    The restore callbacks define the actual behavior of the module.  The server
+    will call them as required to execute restore actions.
+   </para>
+
+   <sect3 id="restore-module-startup">
+    <title>Startup Callback</title>
+
+    <para>
+     The <function>startup_cb</function> callback is called shortly after the
+     module is loaded.  This callback can be used to perform any additional
+     initialization required.  If the restore module has state, it can use
+     <structfield>state->private_data</structfield> to store it.
+
+<programlisting>
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+</programlisting>
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment-configured">
+    <title>Restore WAL Segment Configured Callback</title>
+    <para>
+     The <function>restore_wal_segment_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to retrieve WAL files (e.g., its configuration parameters
+     are set to valid values).  If no
+     <function>restore_wal_segment_configured_cb</function> is defined, the
+     server always assumes the module is not configured to retrieve WAL files.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve WAL files
+     by calling the <function>restore_wal_segment_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_wal_segment_cb</function> callback to retrieve WAL
+     files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment">
+    <title>Restore WAL Segment Callback</title>
+    <para>
+     The <function>restore_wal_segment_cb</function> callback is called to
+     retrieve a single archived segment of the WAL file series for archive
+     recovery or streaming replication.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state, const char *file, const char *path, const char *lastRestartPointFileName);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point.  That is the earliest
+     file that must be kept to allow a restore to be restartable, so this
+     information can be used to truncate the archive to just the minimum
+     required to support restarting from the current restore.
+     <replaceable>lastRestartPointFileName</replaceable> is typically only used
+     by warm-standby configurations (see <xref linkend="warm-standby"/>).  Note
+     that if multiple standby servers are restoring from the same archive
+     directory, you will need to ensure that you do not delete WAL files until
+     they are no longer needed by any of the servers.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history-configured">
+    <title>Restore Timeline History Configured Callback</title>
+    <para>
+     The <function>restore_timeline_history_configured_cb</function> callback
+     is called to determine whether the module is fully configured and ready to
+     accept requests to retrieve timeline history files (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>restore_timeline_history_configured_cb</function> is defined,
+     the server always assumes the module is not configured to retrieve
+     timeline history files.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve timeline
+     history files by calling the
+     <function>restore_timeline_history_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_timeline_history_cb</function> callback to retrieve
+     timeline history files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history">
+    <title>Restore Timeline History Callback</title>
+    <para>
+     The <function>restore_timeline_history_cb</function> callback is called to
+     retrieve a single archived timeline history file for archive recovery or
+     streaming replication.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists-configured">
+    <title>Timeline History Exists Configured Callback</title>
+    <para>
+     The <function>timeline_history_exists_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to determine whether a timeline history file exists in the
+     archives (e.g., its configuration parameters are set to valid values).  If
+     no <function>timeline_history_exists_configured_cb</function> is defined,
+     the server always assumes the module is not configured to check whether
+     certain timeline history files exist.
+
+<programlisting>
+typedef bool (*TimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will check whether
+     certain timeline history files are present in the archives by calling the
+     <function>timeline_history_exists_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>timeline_history_exists_cb</function> callback to check if
+     timeline history files exist in the archives.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists">
+    <title>Timeline History Exists Callback</title>
+    <para>
+     The <function>timeline_history_exists_cb</function> callback is called to
+     check whether a timeline history file is present in the archives.
+
+<programlisting>
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file is
+     present in the archives.  If the file is not available in the archives,
+     the callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the timeline history file.
+    </para>
+
+    <para>
+     Some restore modules might not have a dedicated way to determine whether a
+     timeline history file exists.  For example,
+     <varname>restore_command</varname> only tells the server how to retrieve
+     files.  In this case, the <function>timeline_history_exists_cb</function>
+     callback should copy the file to <replaceable>path</replaceable>, which
+     contains the destination's relative path (including the file name), and
+     the restore module should set
+     <structfield>state->timeline_history_exists_cb_copies</structfield> to
+     <literal>true</literal>.  It is recommended to set this flag in the
+     module's <function>startup_cb</function> callback.  This flag tells the
+     server that it should verify that the file was successfully retrieved
+     after invoking this callback.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup-configured">
+    <title>Archive Cleanup Configured Callback</title>
+    <para>
+     The <function>archive_cleanup_configured_cb</function> callback is called
+     to determine whether the module is fully configured and ready to accept
+     requests to remove old WAL files from the archives (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>archive_cleanup_configured_cb</function> is defined, the server
+     always assumes the module is not configured to remove old WAL files.
+
+<programlisting>
+typedef bool (*ArchiveCleanupConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will remove old WAL
+     files by calling the <function>archive_cleanup_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>archive_cleanup_cb</function> callback to remove old WAL files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup">
+    <title>Archive Cleanup Callback</title>
+    <para>
+     The <function>archive_cleanup_cb</function> callback is called at every
+     restart point by the checkpointer process and is intended to provide a
+     mechanism for cleaning up old archived WAL files that are no longer
+     needed by the standby server.
+
+<programlisting>
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the
+     name of the file that includes the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end-configured">
+    <title>Recovery End Configured Callback</title>
+    <para>
+     The <function>recovery_end_configured_cb</function> callback is called to
+     determine whether the module is fully configured and ready to execute its
+     <function>recovery_end_cb</function> callback once at the end of recovery
+     (e.g., its configuration parameters are set to valid values).  If no
+     <function>recovery_end_configured_cb</function> is defined, the server
+     always assumes the module is not configured to execute its
+     <function>recovery_end_cb</function> callback.
+
+<programlisting>
+typedef bool (*RecoveryEndConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will execute the
+     <function>recovery_end_cb</function> callback once at the end of recovery.
+     If <literal>false</literal> is returned, the server will not use the
+     <function>recovery_end_cb</function> callback at the end of recovery.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end">
+    <title>Recovery End Callback</title>
+    <para>
+     The <function>recovery_end_cb</function> callback is called once at the
+     end of recovery and is intended to provide a mechanism for cleanup
+     following the end of replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-shutdown">
+    <title>Shutdown Callback</title>
+    <para>
+     The <function>shutdown_cb</function> callback is called when a process
+     that has loaded the restore module exits (e.g., after an error) or the
+     value of <xref linkend="guc-restore-library"/> changes.  If no
+     <function>shutdown_cb</function> is defined, no special action is taken in
+     these situations.  If the restore module has state, this callback should
+     free it to avoid leaks.
+
+<programlisting>
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+ </programlisting>
+    </para>
+   </sect3>
+  </sect2>
+ </sect1>
 </chapter>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index b3468eea3c..e1643f92e0 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1261,9 +1261,26 @@ SELECT * FROM pg_backup_stop(wait_for_archive => true);
    <para>
     The key part of all this is to set up a recovery configuration that
     describes how you want to recover and how far the recovery should
-    run.  The one thing that you absolutely must specify is the <varname>restore_command</varname>,
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>,
     which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    WAL file segments.
+   </para>
+
+   <para>
+    Like the <varname>archive_library</varname> parameter,
+    <varname>restore_library</varname> is a shared library.  Since such
+    libraries are written in <literal>C</literal>, creating your own may
+    require considerably more effort than writing a shell command.  However,
+    restore modules can be more performance than restoring via shell, and they
+    will have access to many useful server resources.  For more information
+    about creating a <varname>restore_library</varname>, see
+    <xref linkend="restore-modules"/>.
+   </para>
+
+   <para>
+    Like the <varname>archive_command</varname>,
+    <varname>restore_command</varname> is
     a shell command string.  It can contain <literal>%f</literal>, which is
     replaced by the name of the desired WAL file, and <literal>%p</literal>,
     which is replaced by the path name to copy the WAL file to.
@@ -1282,14 +1299,20 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
    </para>
 
    <para>
-    It is important that the command return nonzero exit status on failure.
-    The command <emphasis>will</emphasis> be called requesting files that are not
-    present in the archive; it must return nonzero when so asked.  This is not
-    an error condition.  An exception is that if the command was terminated by
+    It is important that the <varname>restore_command</varname> return nonzero
+    exit status on failure, or, if you are using a
+    <varname>restore_library</varname>, that the restore callbacks return
+    <literal>false</literal> on failure.  The command or library
+    <emphasis>will</emphasis> be called requesting files that are not
+    present in the archive; it must fail when so asked.  This is not
+    an error condition.  An exception is that if the
+    <varname>restore_command</varname> was terminated by
     a signal (other than <systemitem>SIGTERM</systemitem>, which is used as
     part of a database server shutdown) or an error by the shell (such as
     command not found), then recovery will abort and the server will not start
-    up.
+    up.  Likewise, if the restore callbacks provided by the
+    <varname>restore_library</varname> emit an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1313,7 +1336,8 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
     close as possible given the available WAL segments).  Therefore, a normal
     recovery will end with a <quote>file not found</quote> message, the exact text
     of the error message depending upon your choice of
-    <varname>restore_command</varname>.  You may also see an error message
+    <varname>restore_command</varname> or <varname>restore_library</varname>.
+    You may also see an error message
     at the start of recovery for a file named something like
     <filename>00000001.history</filename>.  This is also normal and does not
     indicate a problem in simple recovery situations; see
diff --git a/doc/src/sgml/basic-archive.sgml b/doc/src/sgml/basic-archive.sgml
index b4d43ced20..f3a5a502bd 100644
--- a/doc/src/sgml/basic-archive.sgml
+++ b/doc/src/sgml/basic-archive.sgml
@@ -8,17 +8,20 @@
  </indexterm>
 
  <para>
-  <filename>basic_archive</filename> is an example of an archive module.  This
-  module copies completed WAL segment files to the specified directory.  This
-  may not be especially useful, but it can serve as a starting point for
-  developing your own archive module.  For more information about archive
-  modules, see <xref linkend="archive-modules"/>.
+  <filename>basic_archive</filename> is an example of an archive and restore
+  module.  This module copies completed WAL segment files to or from the
+  specified directory.  This may not be especially useful, but it can serve as
+  a starting point for developing your own archive and restore modules.  For
+  more information about archive and restore modules, see\
+  <xref linkend="archive-and-restore-modules"/>.
  </para>
 
  <para>
-  In order to function, this module must be loaded via
+  For use as an archive module, this module must be loaded via
   <xref linkend="guc-archive-library"/>, and <xref linkend="guc-archive-mode"/>
-  must be enabled.
+  must be enabled.  For use as a restore module, this module must be loaded
+  via <xref linkend="guc-restore-library"/>, and recovery must be enabled (see
+  <xref linkend="runtime-config-wal-archive-recovery"/>).
  </para>
 
  <sect2 id="basic-archive-configuration-parameters">
@@ -34,11 +37,12 @@
     </term>
     <listitem>
      <para>
-      The directory where the server should copy WAL segment files.  This
-      directory must already exist.  The default is an empty string, which
-      effectively halts WAL archiving, but if <xref linkend="guc-archive-mode"/>
-      is enabled, the server will accumulate WAL segment files in the
-      expectation that a value will soon be provided.
+      The directory where the server should copy WAL segment files to or from.
+      This directory must already exist.  The default is an empty string,
+      which, when used for archiving, effectively halts WAL archival, but if
+      <xref linkend="guc-archive-mode"/> is enabled, the server will accumulate
+      WAL segment files in the expectation that a value will soon be provided.
+      When an empty string is used for recovery, restore will fail.
      </para>
     </listitem>
    </varlistentry>
@@ -46,7 +50,7 @@
 
   <para>
    These parameters must be set in <filename>postgresql.conf</filename>.
-   Typical usage might be:
+   Typical usage as an archive module might be:
   </para>
 
 <programlisting>
@@ -61,6 +65,7 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
+   When <filename>basic_archive</filename> is used as an archive module,
    Server crashes may leave temporary files with the prefix
    <filename>archtemp</filename> in the archive directory.  It is recommended to
    delete such files before restarting the server after a crash.  It is safe to
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index b38cbd714a..bba79e19ab 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3906,7 +3906,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
@@ -3934,7 +3935,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,
@@ -3969,7 +3971,42 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
 
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>restore_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-restore-library" xreflabel="restore_library">
+      <term><varname>restore_library</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>restore_library</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        The library to use for restore actions, including retrieving archived
+        files and executing tasks at restartpoints and at recovery end.  Either
+        <xref linkend="guc-restore-command"/> or
+        <varname>restore_library</varname> is required for archive recovery,
+        but optional for streaming replication.  If this parameter is set to an
+        empty string (the default), restoring via shell is enabled, and
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, and
+        <varname>recovery_end_command</varname> are used.  If both
+        <varname>restore_library</varname> and any of
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, or
+        <varname>recovery_end_command</varname> are set, an error will be
+        raised.  Otherwise, the specified shared library is used for restoring.
+        For more information, see <xref linkend="restore-modules"/>.
+       </para>
+
+       <para>
+        This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
        </para>
       </listitem>
      </varlistentry>
@@ -4014,7 +4051,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>
@@ -4043,7 +4083,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
        </para>
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>recovery_end_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index 236c0af65f..f811398fa2 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -627,7 +627,8 @@ protocol to make nodes agree on a serializable transactional order.
    <para>
     In standby mode, the server continuously applies WAL received from the
     primary server. The standby server can read WAL from a WAL archive
-    (see <xref linkend="guc-restore-command"/>) or directly from the primary
+    (see <xref linkend="guc-restore-command"/> and
+    <xref linkend="guc-restore-library"/>) or directly from the primary
     over a TCP connection (streaming replication). The standby server will
     also attempt to restore any WAL found in the standby cluster's
     <filename>pg_wal</filename> directory. That typically happens after a server
@@ -638,8 +639,10 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     At startup, the standby begins by restoring all WAL available in the
-    archive location, calling <varname>restore_command</varname>. Once it
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s callbacks.  Once it
     reaches the end of WAL available there and <varname>restore_command</varname>
+    or <varname>restore_library</varname>
     fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
     If that fails, and streaming replication has been configured, the
     standby tries to connect to the primary server and start streaming WAL
@@ -698,7 +701,8 @@ protocol to make nodes agree on a serializable transactional order.
     server (see <xref linkend="backup-pitr-recovery"/>). Create a file
     <link linkend="file-standby-signal"><filename>standby.signal</filename></link><indexterm><primary>standby.signal</primary></indexterm>
     in the standby's cluster data
-    directory. Set <xref linkend="guc-restore-command"/> to a simple command to copy files from
+    directory. Set <xref linkend="guc-restore-command"/> or
+    <xref linkend="guc-restore-library"/> to copy files from
     the WAL archive. If you plan to have multiple standby servers for high
     availability purposes, make sure that <varname>recovery_target_timeline</varname> is set to
     <literal>latest</literal> (the default), to make the standby server follow the timeline change
@@ -707,7 +711,8 @@ protocol to make nodes agree on a serializable transactional order.
 
    <note>
      <para>
-     <xref linkend="guc-restore-command"/> should return immediately
+     <xref linkend="guc-restore-command"/> and
+     <xref linkend="guc-restore-library"/> should return immediately
      if the file does not exist; the server will retry the command again if
      necessary.
     </para>
@@ -731,8 +736,9 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     If you're using a WAL archive, its size can be minimized using the <xref
-    linkend="guc-archive-cleanup-command"/> parameter to remove files that are no
-    longer required by the standby server.
+    linkend="guc-archive-cleanup-command"/> parameter or the
+    <xref linkend="guc-restore-library"/>'s archive cleanup callbacks to remove
+    files that are no longer required by the standby server.
     The <application>pg_archivecleanup</application> utility is designed specifically to
     be used with <varname>archive_cleanup_command</varname> in typical single-standby
     configurations, see <xref linkend="pgarchivecleanup"/>.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index e5d12f1d15..34a21c40a5 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -83,7 +83,7 @@
 #include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
-#include "restore/shell_restore.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
@@ -5178,18 +5178,19 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during
+	 * The callback is provided the archive file cutoff point for use during
 	 * log shipping replication.  All files earlier than this point can be
 	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_recovery_end_configured())
+	if (recovery_end_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_recovery_end(lastRestartPointFname);
+		RestoreCallbacks->recovery_end_cb(restore_module_state,
+										  lastRestartPointFname);
 	}
 
 	/*
@@ -7673,18 +7674,19 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Finally, execute archive-cleanup callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during
+	 * The callback is provided the archive file cutoff point for use during
 	 * log shipping replication.  All files earlier than this point can be
 	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_archive_cleanup_configured())
+	if (archive_cleanup_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_archive_cleanup(lastRestartPointFname);
+		RestoreCallbacks->archive_cleanup_cb(restore_module_state,
+											 lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index 320fa857ea..8ebbd6ce50 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -23,14 +23,21 @@
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
 #include "common/archive.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/startup.h"
 #include "postmaster/pgarch.h"
 #include "replication/walsender.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
+#include "utils/memutils.h"
+
+char	   *restoreLibrary = "";
+const RestoreModuleCallbacks *RestoreCallbacks = NULL;
+RestoreModuleState *restore_module_state;
 
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
@@ -74,15 +81,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			if (!shell_restore_file_configured())
+			if (!restore_wal_segment_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			if (!shell_restore_file_configured())
+			if (!restore_timeline_history_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			if (!shell_restore_file_configured())
+			if (!timeline_history_exists_configured())
 				goto not_available;
 			break;
 	}
@@ -174,14 +181,17 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			ret = shell_restore_wal_segment(xlogfname, xlogpath,
-											lastRestartPointFname);
+			ret = RestoreCallbacks->restore_wal_segment_cb(restore_module_state,
+														   xlogfname, xlogpath,
+														   lastRestartPointFname);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->restore_timeline_history_cb(restore_module_state,
+																xlogfname, xlogpath);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->timeline_history_exists_cb(restore_module_state,
+															   xlogfname, xlogpath);
 			break;
 	}
 
@@ -189,6 +199,16 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 
 	if (ret)
 	{
+		/*
+		 * Some restore functions might not copy the file (e.g., checking
+		 * whether a timeline history file exists), but they can set a flag to
+		 * tell us if they do.  We only need to verify file existence if this
+		 * flag is enabled.
+		 */
+		if (archive_type == ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS &&
+			!restore_module_state->timeline_history_exists_cb_copies)
+			return true;
+
 		/*
 		 * command apparently succeeded, but let's make sure the file is
 		 * really there now and has the correct size.
@@ -630,3 +650,65 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the restore callbacks into our global RestoreCallbacks.  The
+ * caller is responsible for validating the combination of library/command
+ * parameters that are set (e.g., restore_command and restore_library cannot
+ * both be set).
+ */
+void
+LoadRestoreCallbacks(void)
+{
+	RestoreModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_restore_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RestoreModuleInit)
+			load_external_function(restoreLibrary, "_PG_restore_module_init",
+								   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("restore modules have to define the symbol "
+						"_PG_restore_module_init")));
+
+	RestoreCallbacks = (*init) ();
+
+	/* restore state should be freed before calling this function */
+	Assert(restore_module_state == NULL);
+	restore_module_state = (RestoreModuleState *)
+		MemoryContextAllocZero(TopMemoryContext,
+							   sizeof(RestoreModuleState));
+
+	if (RestoreCallbacks->startup_cb != NULL)
+		RestoreCallbacks->startup_cb(restore_module_state);
+}
+
+/*
+ * Call the shutdown callback of the loaded restore module, if defined.  Also,
+ * free the restore module state if it was allocated.
+ *
+ * Processes that load restore libraries should register this via
+ * before_shmem_exit().
+ */
+void
+call_restore_module_shutdown_cb(int code, Datum arg)
+{
+	if (RestoreCallbacks != NULL &&
+		RestoreCallbacks->shutdown_cb != NULL &&
+		restore_module_state != NULL)
+		RestoreCallbacks->shutdown_cb(restore_module_state);
+
+	if (restore_module_state != NULL)
+	{
+		pfree(restore_module_state);
+		restore_module_state = NULL;
+	}
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 0bfa569502..4afdcc3aad 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -51,6 +51,7 @@
 #include "replication/slot.h"
 #include "replication/slotsync.h"
 #include "replication/walreceiver.h"
+#include "restore/restore_module.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1114,18 +1115,21 @@ validateRecoveryParameters(void)
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			(!restore_wal_segment_configured() ||
+			 !restore_timeline_history_configured() ||
+			 !timeline_history_exists_configured()))
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
+					(errmsg("specified neither primary_conninfo nor restore_command nor a configured restore_library"),
 					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
 	}
 	else
 	{
-		if (recoveryRestoreCommand == NULL ||
-			strcmp(recoveryRestoreCommand, "") == 0)
+		if (!restore_wal_segment_configured() ||
+			!restore_timeline_history_configured() ||
+			!timeline_history_exists_configured())
 			ereport(FATAL,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("must specify restore_command when standby mode is not enabled")));
+					 errmsg("must specify restore_command or a configured restore_library when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 46197d56f8..47993aa35e 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -45,6 +45,7 @@
 #include "postmaster/bgwriter.h"
 #include "postmaster/interrupt.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -224,6 +225,25 @@ CheckpointerMain(void)
 												 ALLOCSET_DEFAULT_SIZES);
 	MemoryContextSwitchTo(checkpointer_context);
 
+	/*
+	 * Load restore_library so that we can use its archive-cleanup callback,
+	 * if one is defined.
+	 *
+	 * We also take this opportunity to set up the before_shmem_exit hook for
+	 * the shutdown callback and to check that only one of restore_library,
+	 * archive_cleanup_command is set.  Note that we emit a WARNING upon
+	 * detecting misconfigured parameters, and we clear the callbacks so that
+	 * the archive-cleanup functionality is skipped.  We judge this
+	 * functionality to not be important enough to require blocking
+	 * checkpoints or shutting down the server when the parameters are
+	 * misconfigured.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+										 archiveCleanupCommand, "archive_cleanup_command",
+										 WARNING))
+		LoadRestoreCallbacks();
+
 	/*
 	 * If an exception is encountered, processing resumes here.
 	 *
@@ -556,6 +576,10 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		bool		archive_cleanup_settings_changed = false;
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -571,6 +595,36 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		/*
+		 * If the archive-cleanup settings have changed, call the currently
+		 * loaded shutdown callback and clear all the restore callbacks.
+		 * There's presently no good way to unload a library besides
+		 * restarting the process, and there should be little harm in leaving
+		 * it around, so we just leave it loaded.
+		 */
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			archive_cleanup_settings_changed = true;
+			call_restore_module_shutdown_cb(0, (Datum) 0);
+			RestoreCallbacks = NULL;
+		}
+
+		/*
+		 * As in CheckpointerMain(), we only emit a WARNING if we detect that
+		 * both restore_library and archive_cleanup_command are set.  We do
+		 * this even if the archive-cleanup settings haven't changed to remind
+		 * the user about the misconfiguration.
+		 */
+		if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											 archiveCleanupCommand, "archive_cleanup_command",
+											 WARNING) &&
+			archive_cleanup_settings_changed)
+			LoadRestoreCallbacks();
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index e391bf6aa6..3cbd382e00 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -25,6 +25,7 @@
 #include "libpq/pqsignal.h"
 #include "miscadmin.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "storage/ipc.h"
 #include "storage/pmsignal.h"
 #include "storage/procsignal.h"
@@ -118,13 +119,17 @@ StartupProcShutdownHandler(SIGNAL_ARGS)
  * Re-read the config file.
  *
  * If one of the critical walreceiver options has changed, flag xlog.c
- * to restart it.
+ * to restart it.  Also, check for invalid combinations of the command/library
+ * parameters and reload the restore callbacks if necessary.
  */
 static void
 StartupRereadConfig(void)
 {
 	char	   *conninfo = pstrdup(PrimaryConnInfo);
 	char	   *slotname = pstrdup(PrimarySlotName);
+	char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+	char	   *prevRestoreCommand = pstrdup(recoveryRestoreCommand);
+	char	   *prevRecoveryEndCommand = pstrdup(recoveryEndCommand);
 	bool		tempSlot = wal_receiver_create_temp_slot;
 	bool		conninfoChanged;
 	bool		slotnameChanged;
@@ -146,6 +151,30 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	/*
+	 * If the restore settings have changed, call the currently loaded
+	 * shutdown callback and load the new callbacks.  There's presently no
+	 * good way to unload a library besides restarting the process, and there
+	 * should be little harm in leaving it around, so we just leave it loaded.
+	 */
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_restore_module_shutdown_cb(0, (Datum) 0);
+		LoadRestoreCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
@@ -255,6 +284,19 @@ StartupProcessMain(void)
 	 */
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	LoadRestoreCallbacks();
+
 	/*
 	 * Do what we came for.
 	 */
diff --git a/src/backend/restore/shell_restore.c b/src/backend/restore/shell_restore.c
index 42291625c1..be25f04044 100644
--- a/src/backend/restore/shell_restore.c
+++ b/src/backend/restore/shell_restore.c
@@ -3,7 +3,8 @@
  * shell_restore.c
  *
  * These restore functions use a user-specified shell command (e.g., the
- * restore_command GUC).
+ * restore_command GUC).  It is used as the default, but other modules may
+ * define their own restore logic.
  *
  * Copyright (c) 2024, PostgreSQL Global Development Group
  *
@@ -22,25 +23,76 @@
 #include "common/archive.h"
 #include "common/percentrepl.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static void shell_restore_startup(RestoreModuleState *state);
+static bool shell_restore_file_configured(RestoreModuleState *state);
 static bool shell_restore_file(const char *file, const char *path,
 							   const char *lastRestartPointFileName);
+static bool shell_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool shell_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool shell_archive_cleanup_configured(RestoreModuleState *state);
+static void shell_archive_cleanup(RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+static bool shell_recovery_end_configured(RestoreModuleState *state);
+static void shell_recovery_end(RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
 static void ExecuteRecoveryCommand(const char *command,
 								   const char *commandName,
 								   bool failOnSignal,
 								   uint32 wait_event_info,
 								   const char *lastRestartPointFileName);
 
+static const RestoreModuleCallbacks shell_restore_callbacks = {
+	.startup_cb = shell_restore_startup,
+	.restore_wal_segment_configured_cb = shell_restore_file_configured,
+	.restore_wal_segment_cb = shell_restore_wal_segment,
+	.restore_timeline_history_configured_cb = shell_restore_file_configured,
+	.restore_timeline_history_cb = shell_restore_timeline_history,
+	.timeline_history_exists_configured_cb = shell_restore_file_configured,
+	.timeline_history_exists_cb = shell_restore_timeline_history,
+	.archive_cleanup_configured_cb = shell_archive_cleanup_configured,
+	.archive_cleanup_cb = shell_archive_cleanup,
+	.recovery_end_configured_cb = shell_recovery_end_configured,
+	.recovery_end_cb = shell_recovery_end,
+	.shutdown_cb = NULL
+};
+
+/*
+ * shell_restore_init
+ *
+ * Returns the callbacks for restoring via shell.
+ */
+const RestoreModuleCallbacks *
+shell_restore_init(void)
+{
+	return &shell_restore_callbacks;
+}
+
+/*
+ * Indicates that our timeline_history_exists_cb only attempts to retrieve the
+ * file, so the caller should verify the file exists afterwards.
+ */
+static void
+shell_restore_startup(RestoreModuleState *state)
+{
+	state->timeline_history_exists_cb_copies = true;
+}
+
 /*
  * Attempt to execute a shell-based restore command to retrieve a WAL segment.
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_wal_segment(const char *file, const char *path,
+static bool
+shell_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
 						  const char *lastRestartPointFileName)
 {
 	return shell_restore_file(file, path, lastRestartPointFileName);
@@ -52,8 +104,9 @@ shell_restore_wal_segment(const char *file, const char *path,
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_timeline_history(const char *file, const char *path)
+static bool
+shell_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
 {
 	char		lastRestartPointFname[MAXPGPATH];
 
@@ -64,8 +117,8 @@ shell_restore_timeline_history(const char *file, const char *path)
 /*
  * Check whether restore_command is supplied.
  */
-bool
-shell_restore_file_configured(void)
+static bool
+shell_restore_file_configured(RestoreModuleState *state)
 {
 	return recoveryRestoreCommand[0] != '\0';
 }
@@ -152,8 +205,8 @@ shell_restore_file(const char *file, const char *path,
 /*
  * Check whether archive_cleanup_command is supplied.
  */
-bool
-shell_archive_cleanup_configured(void)
+static bool
+shell_archive_cleanup_configured(RestoreModuleState *state)
 {
 	return archiveCleanupCommand[0] != '\0';
 }
@@ -161,8 +214,9 @@ shell_archive_cleanup_configured(void)
 /*
  * Attempt to execute a shell-based archive cleanup command.
  */
-void
-shell_archive_cleanup(const char *lastRestartPointFileName)
+static void
+shell_archive_cleanup(RestoreModuleState *state,
+					  const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(archiveCleanupCommand, "archive_cleanup_command",
 						   false, WAIT_EVENT_ARCHIVE_CLEANUP_COMMAND,
@@ -172,8 +226,8 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 /*
  * Check whether recovery_end_command is supplied.
  */
-bool
-shell_recovery_end_configured(void)
+static bool
+shell_recovery_end_configured(RestoreModuleState *state)
 {
 	return recoveryEndCommand[0] != '\0';
 }
@@ -181,8 +235,9 @@ shell_recovery_end_configured(void)
 /*
  * Attempt to execute a shell-based end-of-recovery command.
  */
-void
-shell_recovery_end(const char *lastRestartPointFileName)
+static void
+shell_recovery_end(RestoreModuleState *state,
+				   const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(recoveryEndCommand, "recovery_end_command", true,
 						   WAIT_EVENT_RECOVERY_END_COMMAND,
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 45013582a7..724d268d7b 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -70,6 +70,7 @@
 #include "replication/slot.h"
 #include "replication/slotsync.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -3946,6 +3947,16 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"restore_library", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
+			gettext_noop("Sets the library that will be called for restore actions."),
+			NULL
+		},
+		&restoreLibrary,
+		"",
+		NULL, NULL, NULL
+	},
+
 	{
 		{"archive_cleanup_command", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
 			gettext_noop("Sets the shell command that will be executed at every restart point."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index edcc0282b2..2939042b06 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -284,6 +284,7 @@
 				# placeholders: %p = path of file to restore
 				#               %f = file name only
 				# e.g. 'cp /mnt/server/archivedir/%f %p'
+#restore_library = ''		# library to use for restore actions
 #archive_cleanup_command = ''	# command to execute at every restartpoint
 #recovery_end_command = ''	# command to execute at completion of recovery
 
diff --git a/src/include/restore/restore_module.h b/src/include/restore/restore_module.h
new file mode 100644
index 0000000000..cc148e855a
--- /dev/null
+++ b/src/include/restore/restore_module.h
@@ -0,0 +1,143 @@
+/*-------------------------------------------------------------------------
+ *
+ * restore_module.h
+ *		Exports for restore modules.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * src/include/restore/restore_module.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef _RESTORE_MODULE_H
+#define _RESTORE_MODULE_H
+
+/*
+ * The value of the restore_library GUC.
+ */
+extern PGDLLIMPORT char *restoreLibrary;
+
+typedef struct RestoreModuleState
+{
+	/*
+	 * Private data pointer for use by a restore module.  This can be used to
+	 * store state for the module that will be passed to each of its
+	 * callbacks.
+	 */
+	void	   *private_data;
+
+	/*
+	 * Indicates whether the callback that checks for timeline existence
+	 * merely copies the file.  If set to true, the server will verify that
+	 * the timeline history file exists after the callback returns.
+	 */
+	bool		timeline_history_exists_cb_copies;
+} RestoreModuleState;
+
+/*
+ * Restore module callbacks
+ *
+ * These callback functions should be defined by restore libraries and returned
+ * via _PG_restore_module_init().  For more information about the purpose of
+ * each callback, refer to the restore modules documentation.
+ */
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state,
+									 const char *file, const char *path,
+									 const char *lastRestartPointFileName);
+typedef bool (*RestoreTimelineHistoryConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state,
+										  const char *file, const char *path);
+typedef bool (*TimelineHistoryExistsConfiguredCB) (RestoreModuleState *state);
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state,
+										 const char *file, const char *path);
+typedef bool (*ArchiveCleanupConfiguredCB) (RestoreModuleState *state);
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+typedef bool (*RecoveryEndConfiguredCB) (RestoreModuleState *state);
+typedef void (*RecoveryEndCB) (RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+
+typedef struct RestoreModuleCallbacks
+{
+	RestoreStartupCB startup_cb;
+	RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+	RestoreWalSegmentCB restore_wal_segment_cb;
+	RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+	RestoreTimelineHistoryCB restore_timeline_history_cb;
+	TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+	TimelineHistoryExistsCB timeline_history_exists_cb;
+	ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+	ArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndConfiguredCB recovery_end_configured_cb;
+	RecoveryEndCB recovery_end_cb;
+	RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+
+/*
+ * Type of the shared library symbol _PG_restore_module_init that is looked up
+ * when loading a restore library.
+ */
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+
+extern PGDLLEXPORT const RestoreModuleCallbacks *_PG_restore_module_init(void);
+
+extern void LoadRestoreCallbacks(void);
+extern void call_restore_module_shutdown_cb(int code, Datum arg);
+
+extern const RestoreModuleCallbacks *RestoreCallbacks;
+extern RestoreModuleState *restore_module_state;
+
+/*
+ * The following *_configured() functions are convenient wrappers around the
+ * *_configured_cb() callback functions with additional defensive NULL checks.
+ */
+
+static inline bool
+restore_wal_segment_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->restore_wal_segment_configured_cb != NULL &&
+		RestoreCallbacks->restore_wal_segment_cb != NULL &&
+		RestoreCallbacks->restore_wal_segment_configured_cb(restore_module_state);
+}
+
+static inline bool
+restore_timeline_history_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->restore_timeline_history_configured_cb != NULL &&
+		RestoreCallbacks->restore_timeline_history_cb != NULL &&
+		RestoreCallbacks->restore_timeline_history_configured_cb(restore_module_state);
+}
+
+static inline bool
+timeline_history_exists_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->timeline_history_exists_configured_cb != NULL &&
+		RestoreCallbacks->timeline_history_exists_cb != NULL &&
+		RestoreCallbacks->timeline_history_exists_configured_cb(restore_module_state);
+}
+
+static inline bool
+archive_cleanup_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->archive_cleanup_configured_cb != NULL &&
+		RestoreCallbacks->archive_cleanup_cb != NULL &&
+		RestoreCallbacks->archive_cleanup_configured_cb(restore_module_state);
+}
+
+static inline bool
+recovery_end_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->recovery_end_configured_cb != NULL &&
+		RestoreCallbacks->recovery_end_cb != NULL &&
+		RestoreCallbacks->recovery_end_configured_cb(restore_module_state);
+}
+
+#endif							/* _RESTORE_MODULE_H */
diff --git a/src/include/restore/shell_restore.h b/src/include/restore/shell_restore.h
index 28b5b7bc92..6352623866 100644
--- a/src/include/restore/shell_restore.h
+++ b/src/include/restore/shell_restore.h
@@ -12,15 +12,13 @@
 #ifndef _SHELL_RESTORE_H
 #define _SHELL_RESTORE_H
 
-extern bool shell_restore_file_configured(void);
-extern bool shell_restore_wal_segment(const char *file, const char *path,
-									  const char *lastRestartPointFileName);
-extern bool shell_restore_timeline_history(const char *file, const char *path);
+#include "restore/restore_module.h"
 
-extern bool shell_archive_cleanup_configured(void);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-
-extern bool shell_recovery_end_configured(void);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Since the logic for restoring via shell commands is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern const RestoreModuleCallbacks *shell_restore_init(void);
 
 #endif							/* _SHELL_RESTORE_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3a02310f26..6a05313f6b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2386,6 +2386,8 @@ ResourceOwnerDesc
 ResourceReleaseCallback
 ResourceReleaseCallbackItem
 ResourceReleasePhase
+RestoreModuleCallbacks
+RestoreModuleState
 RestoreOptions
 RestorePass
 RestrictInfo
-- 
2.25.1


--IJpNTDwzlM2Ie8A6--





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

* [PATCH v19 5/5] introduce restore_library
@ 2023-02-16 06:06  Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 28+ messages in thread

From: Nathan Bossart @ 2023-02-16 06:06 UTC (permalink / raw)

---
 contrib/basic_archive/Makefile                |   2 +
 contrib/basic_archive/basic_archive.c         | 153 +++++++-
 contrib/basic_archive/meson.build             |   5 +
 contrib/basic_archive/t/001_restore.pl        |  44 +++
 doc/src/sgml/archive-and-restore-modules.sgml | 356 +++++++++++++++++-
 doc/src/sgml/backup.sgml                      |  40 +-
 doc/src/sgml/basic-archive.sgml               |  31 +-
 doc/src/sgml/config.sgml                      |  53 ++-
 doc/src/sgml/high-availability.sgml           |  18 +-
 src/backend/access/transam/xlog.c             |  20 +-
 src/backend/access/transam/xlogarchive.c      |  96 ++++-
 src/backend/access/transam/xlogrecovery.c     |  14 +-
 src/backend/postmaster/checkpointer.c         |  54 +++
 src/backend/postmaster/startup.c              |  44 ++-
 src/backend/restore/shell_restore.c           |  85 ++++-
 src/backend/utils/misc/guc_tables.c           |  11 +
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/restore/restore_module.h          | 143 +++++++
 src/include/restore/shell_restore.h           |  16 +-
 src/tools/pgindent/typedefs.list              |   2 +
 20 files changed, 1104 insertions(+), 84 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl
 create mode 100644 src/include/restore/restore_module.h

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 100ed81f12..e61f7d676f 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -10,6 +10,8 @@ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.c
 # typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
 
+TAP_TESTS = 1
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index 6b102e9072..e0d8dce364 100644
--- a/contrib/basic_archive/basic_archive.c
+++ b/contrib/basic_archive/basic_archive.c
@@ -17,6 +17,11 @@
  * a file is successfully archived and then the system crashes before
  * a durable record of the success has been made.
  *
+ * This file also demonstrates a basic restore library implementation that
+ * is roughly equivalent to the following shell command:
+ *
+ *		cp /path/to/archivedir/%f %p
+ *
  * Copyright (c) 2022-2024, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
@@ -33,6 +38,7 @@
 #include "archive/archive_module.h"
 #include "common/int.h"
 #include "miscadmin.h"
+#include "restore/restore_module.h"
 #include "storage/copydir.h"
 #include "storage/fd.h"
 #include "utils/guc.h"
@@ -54,6 +60,16 @@ static void basic_archive_file_internal(const char *file, const char *path);
 static bool check_archive_directory(char **newval, void **extra, GucSource source);
 static bool compare_files(const char *file1, const char *file2);
 static void basic_archive_shutdown(ArchiveModuleState *state);
+static bool basic_restore_configured(RestoreModuleState *state);
+static bool basic_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool basic_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool basic_restore_file(const char *file, const char *path);
+static bool basic_restore_timeline_history_exists(RestoreModuleState *state,
+												  const char *file,
+												  const char *path);
 
 static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.startup_cb = basic_archive_startup,
@@ -62,6 +78,21 @@ static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.shutdown_cb = basic_archive_shutdown
 };
 
+static const RestoreModuleCallbacks basic_restore_callbacks = {
+	.startup_cb = NULL,
+	.restore_wal_segment_configured_cb = basic_restore_configured,
+	.restore_wal_segment_cb = basic_restore_wal_segment,
+	.restore_timeline_history_configured_cb = basic_restore_configured,
+	.restore_timeline_history_cb = basic_restore_timeline_history,
+	.timeline_history_exists_configured_cb = basic_restore_configured,
+	.timeline_history_exists_cb = basic_restore_timeline_history_exists,
+	.archive_cleanup_configured_cb = NULL,
+	.archive_cleanup_cb = NULL,
+	.recovery_end_configured_cb = NULL,
+	.recovery_end_cb = NULL,
+	.shutdown_cb = NULL
+};
+
 /*
  * _PG_init
  *
@@ -71,7 +102,7 @@ void
 _PG_init(void)
 {
 	DefineCustomStringVariable("basic_archive.archive_directory",
-							   gettext_noop("Archive file destination directory."),
+							   gettext_noop("Archive file source/destination directory."),
 							   NULL,
 							   &archive_directory,
 							   "",
@@ -93,6 +124,17 @@ _PG_archive_module_init(void)
 	return &basic_archive_callbacks;
 }
 
+/*
+ * _PG_restore_module_init
+ *
+ * Returns the module's restore callbacks.
+ */
+const RestoreModuleCallbacks *
+_PG_restore_module_init(void)
+{
+	return &basic_restore_callbacks;
+}
+
 /*
  * basic_archive_startup
  *
@@ -431,3 +473,112 @@ basic_archive_shutdown(ArchiveModuleState *state)
 	pfree(data);
 	state->private_data = NULL;
 }
+
+/*
+ * basic_restore_configured
+ *
+ * Checks that archive_directory is not blank.
+ */
+static bool
+basic_restore_configured(RestoreModuleState *state)
+{
+	return archive_directory != NULL && archive_directory[0] != '\0';
+}
+
+/*
+ * basic_restore_wal_segment
+ *
+ * Retrieves one archived WAL segment.
+ */
+static bool
+basic_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
+						  const char *lastRestartPointFileName)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_timeline_history
+ *
+ * Retrieves one timeline history file.
+ */
+static bool
+basic_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file.
+ */
+static bool
+basic_restore_file(const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive",
+					file)));
+
+	/*
+	 * If the file doesn't exist, return false to indicate that there are no
+	 * more files to restore.
+	 */
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	copy_file(archive_path, path);
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive",
+					file)));
+	return true;
+}
+
+/*
+ * basic_restore_timeline_history_exists
+ *
+ * Check whether a timeline history file is present in the archive directory.
+ */
+static bool
+basic_restore_timeline_history_exists(RestoreModuleState *state,
+									  const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("checking existence of \"%s\" via basic_archive",
+					file)));
+
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	ereport(DEBUG1,
+			(errmsg("verified existence of \"%s\" via basic_archive",
+					file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index cc2f62bf36..8e36fa7ed6 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -31,4 +31,9 @@ tests += {
     # typical installcheck users do not have (e.g. buildfarm clients).
     'runningcheck': false,
   },
+  'tap': {
+    'tests': [
+      't/001_restore.pl',
+    ],
+  },
 }
diff --git a/contrib/basic_archive/t/001_restore.pl b/contrib/basic_archive/t/001_restore.pl
new file mode 100644
index 0000000000..6c01f736cf
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,44 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# start a node
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init(has_archiving => 1, allows_streaming => 1);
+my $archive_dir = $node->archive_dir;
+$archive_dir =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
+$node->append_conf('postgresql.conf', "archive_command = ''");
+$node->append_conf('postgresql.conf', "archive_library = 'basic_archive'");
+$node->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$node->start;
+
+# backup the node
+my $backup = 'backup';
+$node->backup($backup);
+
+# generate some new WAL files
+$node->safe_psql('postgres', "CREATE TABLE test (a INT);");
+$node->safe_psql('postgres', "SELECT pg_switch_wal();");
+$node->safe_psql('postgres', "INSERT INTO test VALUES (1);");
+
+# shut down the node (this should archive all WAL files)
+$node->stop;
+
+# restore from the backup
+my $restore = PostgreSQL::Test::Cluster->new('restore');
+$restore->init_from_backup($node, $backup, has_restoring => 1, standby => 0);
+$restore->append_conf('postgresql.conf', "restore_command = ''");
+$restore->append_conf('postgresql.conf', "restore_library = 'basic_archive'");
+$restore->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$restore->start;
+
+# ensure post-backup WAL is replayed
+my $result = $restore->poll_query_until("postgres", "SELECT count(*) = 1 FROM test;");
+is($result, "1", "check restore content");
+
+done_testing();
diff --git a/doc/src/sgml/archive-and-restore-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml
index a52d15082d..15be548a73 100644
--- a/doc/src/sgml/archive-and-restore-modules.sgml
+++ b/doc/src/sgml/archive-and-restore-modules.sgml
@@ -8,15 +8,17 @@
 
  <para>
   PostgreSQL provides infrastructure to create custom modules for continuous
-  archiving (see <xref linkend="continuous-archiving"/>).  While archiving via
-  a shell command (i.e., <xref linkend="guc-archive-command"/>) is much
-  simpler, a custom archive module will often be considerably more robust and
-  performant.
+  archiving and recovery (see <xref linkend="continuous-archiving"/>).  While a
+  shell command (i.e., <xref linkend="guc-archive-command"/>,
+  <xref linkend="guc-restore-command"/>) is much simpler, a custom module will
+  often be considerably more robust and performant.
  </para>
 
  <para>
   The <filename>contrib/basic_archive</filename> module contains a working
-  example, which demonstrates some useful techniques.
+  example, which demonstrates some useful techniques.  As demonstrated in
+  <filename>basic_archive</filename> a single module may serve as both an
+  archive module and a restore module.
  </para>
 
  <sect1 id="archive-modules">
@@ -178,4 +180,348 @@ typedef void (*ArchiveShutdownCB) (ArchiveModuleState *state);
    </sect3>
   </sect2>
  </sect1>
+
+ <sect1 id="restore-modules">
+  <title>Restore Modules</title>
+  <indexterm zone="restore-modules">
+   <primary>Restore Modules</primary>
+  </indexterm>
+
+  <para>
+   When a custom <xref linkend="guc-restore-library"/> is configured,
+   PostgreSQL will use the module for restore actions.  It is
+   ultimately up to the module to decide how to accomplish each task,
+   but some recommendations are listed at
+   <xref linkend="backup-pitr-recovery"/>.
+  </para>
+
+  <para>
+   Restore modules must at least consist of an initialization function
+   (see <xref linkend="restore-module-init"/>) and the required callbacks (see
+   <xref linkend="restore-module-callbacks"/>).  However, restore modules are
+   also permitted to do much more (e.g., declare GUCs and register background
+   workers).
+  </para>
+
+  <sect2 id="restore-module-init">
+   <title>Initialization Functions</title>
+   <indexterm zone="restore-module-init">
+    <primary>_PG_restore_module_init</primary>
+   </indexterm>
+
+   <para>
+    An restore library is loaded by dynamically loading a shared library with
+    the <xref linkend="guc-restore-library"/>'s name as the library base name.
+    The normal library search path is used to locate the library.  To provide
+    the required restore module callbacks and to indicate that the library is
+    actually a restore module, it needs to provide a function named
+    <function>_PG_restore_module_init</function>.  The result of the function
+    must be a pointer to a struct of type
+    <structname>RestoreModuleCallbacks</structname>, which contains everything
+    that the core code needs to know how to make use of the restore module.
+    The return value needs to be of server lifetime, which is typically
+    achieved by defining it as a <literal>static const</literal> variable in
+    global scope.
+
+<programlisting>
+typedef struct RestoreModuleCallbacks
+{
+    RestoreStartupCB startup_cb;
+    RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+    RestoreWalSegmentCB restore_wal_segment_cb;
+    RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+    RestoreTimelineHistoryCB restore_timeline_history_cb;
+    TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+    TimelineHistoryExistsCB timeline_history_exists_cb;
+    ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+    ArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndConfiguredCB recovery_end_configured_cb;
+    RecoveryEndCB recovery_end_cb;
+    RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+</programlisting>
+
+    The <function>restore_wal_segment_configured_cb</function>,
+    <function>restore_wal_segment_cb</function>,
+    <function>restore_timeline_history_configured_cb</function>,
+    <function>restore_timeline_history_cb</function>,
+    <function>timeline_history_exists_configured_cb</function>, and
+    <function>timeline_history_exists_cb</function> callbacks are required for
+    archive recovery but optional for streaming replication.  The others are
+    always optional.
+   </para>
+  </sect2>
+
+  <sect2 id="restore-module-callbacks">
+   <title>Restore Module Callbacks</title>
+
+   <para>
+    The restore callbacks define the actual behavior of the module.  The server
+    will call them as required to execute restore actions.
+   </para>
+
+   <sect3 id="restore-module-startup">
+    <title>Startup Callback</title>
+
+    <para>
+     The <function>startup_cb</function> callback is called shortly after the
+     module is loaded.  This callback can be used to perform any additional
+     initialization required.  If the restore module has state, it can use
+     <structfield>state->private_data</structfield> to store it.
+
+<programlisting>
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+</programlisting>
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment-configured">
+    <title>Restore WAL Segment Configured Callback</title>
+    <para>
+     The <function>restore_wal_segment_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to retrieve WAL files (e.g., its configuration parameters
+     are set to valid values).  If no
+     <function>restore_wal_segment_configured_cb</function> is defined, the
+     server always assumes the module is not configured to retrieve WAL files.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve WAL files
+     by calling the <function>restore_wal_segment_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_wal_segment_cb</function> callback to retrieve WAL
+     files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment">
+    <title>Restore WAL Segment Callback</title>
+    <para>
+     The <function>restore_wal_segment_cb</function> callback is called to
+     retrieve a single archived segment of the WAL file series for archive
+     recovery or streaming replication.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state, const char *file, const char *path, const char *lastRestartPointFileName);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point.  That is the earliest
+     file that must be kept to allow a restore to be restartable, so this
+     information can be used to truncate the archive to just the minimum
+     required to support restarting from the current restore.
+     <replaceable>lastRestartPointFileName</replaceable> is typically only used
+     by warm-standby configurations (see <xref linkend="warm-standby"/>).  Note
+     that if multiple standby servers are restoring from the same archive
+     directory, you will need to ensure that you do not delete WAL files until
+     they are no longer needed by any of the servers.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history-configured">
+    <title>Restore Timeline History Configured Callback</title>
+    <para>
+     The <function>restore_timeline_history_configured_cb</function> callback
+     is called to determine whether the module is fully configured and ready to
+     accept requests to retrieve timeline history files (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>restore_timeline_history_configured_cb</function> is defined,
+     the server always assumes the module is not configured to retrieve
+     timeline history files.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve timeline
+     history files by calling the
+     <function>restore_timeline_history_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_timeline_history_cb</function> callback to retrieve
+     timeline history files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history">
+    <title>Restore Timeline History Callback</title>
+    <para>
+     The <function>restore_timeline_history_cb</function> callback is called to
+     retrieve a single archived timeline history file for archive recovery or
+     streaming replication.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists-configured">
+    <title>Timeline History Exists Configured Callback</title>
+    <para>
+     The <function>timeline_history_exists_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to determine whether a timeline history file exists in the
+     archives (e.g., its configuration parameters are set to valid values).  If
+     no <function>timeline_history_exists_configured_cb</function> is defined,
+     the server always assumes the module is not configured to check whether
+     certain timeline history files exist.
+
+<programlisting>
+typedef bool (*TimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will check whether
+     certain timeline history files are present in the archives by calling the
+     <function>timeline_history_exists_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>timeline_history_exists_cb</function> callback to check if
+     timeline history files exist in the archives.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists">
+    <title>Timeline History Exists Callback</title>
+    <para>
+     The <function>timeline_history_exists_cb</function> callback is called to
+     check whether a timeline history file is present in the archives.
+
+<programlisting>
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file is
+     present in the archives.  If the file is not available in the archives,
+     the callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the timeline history file.
+    </para>
+
+    <para>
+     Some restore modules might not have a dedicated way to determine whether a
+     timeline history file exists.  For example,
+     <varname>restore_command</varname> only tells the server how to retrieve
+     files.  In this case, the <function>timeline_history_exists_cb</function>
+     callback should copy the file to <replaceable>path</replaceable>, which
+     contains the destination's relative path (including the file name), and
+     the restore module should set
+     <structfield>state->timeline_history_exists_cb_copies</structfield> to
+     <literal>true</literal>.  It is recommended to set this flag in the
+     module's <function>startup_cb</function> callback.  This flag tells the
+     server that it should verify that the file was successfully retrieved
+     after invoking this callback.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup-configured">
+    <title>Archive Cleanup Configured Callback</title>
+    <para>
+     The <function>archive_cleanup_configured_cb</function> callback is called
+     to determine whether the module is fully configured and ready to accept
+     requests to remove old WAL files from the archives (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>archive_cleanup_configured_cb</function> is defined, the server
+     always assumes the module is not configured to remove old WAL files.
+
+<programlisting>
+typedef bool (*ArchiveCleanupConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will remove old WAL
+     files by calling the <function>archive_cleanup_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>archive_cleanup_cb</function> callback to remove old WAL files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup">
+    <title>Archive Cleanup Callback</title>
+    <para>
+     The <function>archive_cleanup_cb</function> callback is called at every
+     restart point by the checkpointer process and is intended to provide a
+     mechanism for cleaning up old archived WAL files that are no longer
+     needed by the standby server.
+
+<programlisting>
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the
+     name of the file that includes the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end-configured">
+    <title>Recovery End Configured Callback</title>
+    <para>
+     The <function>recovery_end_configured_cb</function> callback is called to
+     determine whether the module is fully configured and ready to execute its
+     <function>recovery_end_cb</function> callback once at the end of recovery
+     (e.g., its configuration parameters are set to valid values).  If no
+     <function>recovery_end_configured_cb</function> is defined, the server
+     always assumes the module is not configured to execute its
+     <function>recovery_end_cb</function> callback.
+
+<programlisting>
+typedef bool (*RecoveryEndConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will execute the
+     <function>recovery_end_cb</function> callback once at the end of recovery.
+     If <literal>false</literal> is returned, the server will not use the
+     <function>recovery_end_cb</function> callback at the end of recovery.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end">
+    <title>Recovery End Callback</title>
+    <para>
+     The <function>recovery_end_cb</function> callback is called once at the
+     end of recovery and is intended to provide a mechanism for cleanup
+     following the end of replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-shutdown">
+    <title>Shutdown Callback</title>
+    <para>
+     The <function>shutdown_cb</function> callback is called when a process
+     that has loaded the restore module exits (e.g., after an error) or the
+     value of <xref linkend="guc-restore-library"/> changes.  If no
+     <function>shutdown_cb</function> is defined, no special action is taken in
+     these situations.  If the restore module has state, this callback should
+     free it to avoid leaks.
+
+<programlisting>
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+ </programlisting>
+    </para>
+   </sect3>
+  </sect2>
+ </sect1>
 </chapter>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index b3468eea3c..e1643f92e0 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1261,9 +1261,26 @@ SELECT * FROM pg_backup_stop(wait_for_archive => true);
    <para>
     The key part of all this is to set up a recovery configuration that
     describes how you want to recover and how far the recovery should
-    run.  The one thing that you absolutely must specify is the <varname>restore_command</varname>,
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>,
     which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    WAL file segments.
+   </para>
+
+   <para>
+    Like the <varname>archive_library</varname> parameter,
+    <varname>restore_library</varname> is a shared library.  Since such
+    libraries are written in <literal>C</literal>, creating your own may
+    require considerably more effort than writing a shell command.  However,
+    restore modules can be more performance than restoring via shell, and they
+    will have access to many useful server resources.  For more information
+    about creating a <varname>restore_library</varname>, see
+    <xref linkend="restore-modules"/>.
+   </para>
+
+   <para>
+    Like the <varname>archive_command</varname>,
+    <varname>restore_command</varname> is
     a shell command string.  It can contain <literal>%f</literal>, which is
     replaced by the name of the desired WAL file, and <literal>%p</literal>,
     which is replaced by the path name to copy the WAL file to.
@@ -1282,14 +1299,20 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
    </para>
 
    <para>
-    It is important that the command return nonzero exit status on failure.
-    The command <emphasis>will</emphasis> be called requesting files that are not
-    present in the archive; it must return nonzero when so asked.  This is not
-    an error condition.  An exception is that if the command was terminated by
+    It is important that the <varname>restore_command</varname> return nonzero
+    exit status on failure, or, if you are using a
+    <varname>restore_library</varname>, that the restore callbacks return
+    <literal>false</literal> on failure.  The command or library
+    <emphasis>will</emphasis> be called requesting files that are not
+    present in the archive; it must fail when so asked.  This is not
+    an error condition.  An exception is that if the
+    <varname>restore_command</varname> was terminated by
     a signal (other than <systemitem>SIGTERM</systemitem>, which is used as
     part of a database server shutdown) or an error by the shell (such as
     command not found), then recovery will abort and the server will not start
-    up.
+    up.  Likewise, if the restore callbacks provided by the
+    <varname>restore_library</varname> emit an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1313,7 +1336,8 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
     close as possible given the available WAL segments).  Therefore, a normal
     recovery will end with a <quote>file not found</quote> message, the exact text
     of the error message depending upon your choice of
-    <varname>restore_command</varname>.  You may also see an error message
+    <varname>restore_command</varname> or <varname>restore_library</varname>.
+    You may also see an error message
     at the start of recovery for a file named something like
     <filename>00000001.history</filename>.  This is also normal and does not
     indicate a problem in simple recovery situations; see
diff --git a/doc/src/sgml/basic-archive.sgml b/doc/src/sgml/basic-archive.sgml
index b4d43ced20..f3a5a502bd 100644
--- a/doc/src/sgml/basic-archive.sgml
+++ b/doc/src/sgml/basic-archive.sgml
@@ -8,17 +8,20 @@
  </indexterm>
 
  <para>
-  <filename>basic_archive</filename> is an example of an archive module.  This
-  module copies completed WAL segment files to the specified directory.  This
-  may not be especially useful, but it can serve as a starting point for
-  developing your own archive module.  For more information about archive
-  modules, see <xref linkend="archive-modules"/>.
+  <filename>basic_archive</filename> is an example of an archive and restore
+  module.  This module copies completed WAL segment files to or from the
+  specified directory.  This may not be especially useful, but it can serve as
+  a starting point for developing your own archive and restore modules.  For
+  more information about archive and restore modules, see\
+  <xref linkend="archive-and-restore-modules"/>.
  </para>
 
  <para>
-  In order to function, this module must be loaded via
+  For use as an archive module, this module must be loaded via
   <xref linkend="guc-archive-library"/>, and <xref linkend="guc-archive-mode"/>
-  must be enabled.
+  must be enabled.  For use as a restore module, this module must be loaded
+  via <xref linkend="guc-restore-library"/>, and recovery must be enabled (see
+  <xref linkend="runtime-config-wal-archive-recovery"/>).
  </para>
 
  <sect2 id="basic-archive-configuration-parameters">
@@ -34,11 +37,12 @@
     </term>
     <listitem>
      <para>
-      The directory where the server should copy WAL segment files.  This
-      directory must already exist.  The default is an empty string, which
-      effectively halts WAL archiving, but if <xref linkend="guc-archive-mode"/>
-      is enabled, the server will accumulate WAL segment files in the
-      expectation that a value will soon be provided.
+      The directory where the server should copy WAL segment files to or from.
+      This directory must already exist.  The default is an empty string,
+      which, when used for archiving, effectively halts WAL archival, but if
+      <xref linkend="guc-archive-mode"/> is enabled, the server will accumulate
+      WAL segment files in the expectation that a value will soon be provided.
+      When an empty string is used for recovery, restore will fail.
      </para>
     </listitem>
    </varlistentry>
@@ -46,7 +50,7 @@
 
   <para>
    These parameters must be set in <filename>postgresql.conf</filename>.
-   Typical usage might be:
+   Typical usage as an archive module might be:
   </para>
 
 <programlisting>
@@ -61,6 +65,7 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
+   When <filename>basic_archive</filename> is used as an archive module,
    Server crashes may leave temporary files with the prefix
    <filename>archtemp</filename> in the archive directory.  It is recommended to
    delete such files before restarting the server after a crash.  It is safe to
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 65a6e6c408..9a2570f87f 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3906,7 +3906,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
@@ -3934,7 +3935,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,
@@ -3969,7 +3971,42 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
 
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>restore_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-restore-library" xreflabel="restore_library">
+      <term><varname>restore_library</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>restore_library</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        The library to use for restore actions, including retrieving archived
+        files and executing tasks at restartpoints and at recovery end.  Either
+        <xref linkend="guc-restore-command"/> or
+        <varname>restore_library</varname> is required for archive recovery,
+        but optional for streaming replication.  If this parameter is set to an
+        empty string (the default), restoring via shell is enabled, and
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, and
+        <varname>recovery_end_command</varname> are used.  If both
+        <varname>restore_library</varname> and any of
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, or
+        <varname>recovery_end_command</varname> are set, an error will be
+        raised.  Otherwise, the specified shared library is used for restoring.
+        For more information, see <xref linkend="restore-modules"/>.
+       </para>
+
+       <para>
+        This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
        </para>
       </listitem>
      </varlistentry>
@@ -4014,7 +4051,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>
@@ -4043,7 +4083,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
        </para>
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>recovery_end_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index b48209fc2f..a86a9fe93a 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -627,7 +627,8 @@ protocol to make nodes agree on a serializable transactional order.
    <para>
     In standby mode, the server continuously applies WAL received from the
     primary server. The standby server can read WAL from a WAL archive
-    (see <xref linkend="guc-restore-command"/>) or directly from the primary
+    (see <xref linkend="guc-restore-command"/> and
+    <xref linkend="guc-restore-library"/>) or directly from the primary
     over a TCP connection (streaming replication). The standby server will
     also attempt to restore any WAL found in the standby cluster's
     <filename>pg_wal</filename> directory. That typically happens after a server
@@ -638,8 +639,10 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     At startup, the standby begins by restoring all WAL available in the
-    archive location, calling <varname>restore_command</varname>. Once it
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s callbacks.  Once it
     reaches the end of WAL available there and <varname>restore_command</varname>
+    or <varname>restore_library</varname>
     fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
     If that fails, and streaming replication has been configured, the
     standby tries to connect to the primary server and start streaming WAL
@@ -698,7 +701,8 @@ protocol to make nodes agree on a serializable transactional order.
     server (see <xref linkend="backup-pitr-recovery"/>). Create a file
     <link linkend="file-standby-signal"><filename>standby.signal</filename></link><indexterm><primary>standby.signal</primary></indexterm>
     in the standby's cluster data
-    directory. Set <xref linkend="guc-restore-command"/> to a simple command to copy files from
+    directory. Set <xref linkend="guc-restore-command"/> or
+    <xref linkend="guc-restore-library"/> to copy files from
     the WAL archive. If you plan to have multiple standby servers for high
     availability purposes, make sure that <varname>recovery_target_timeline</varname> is set to
     <literal>latest</literal> (the default), to make the standby server follow the timeline change
@@ -707,7 +711,8 @@ protocol to make nodes agree on a serializable transactional order.
 
    <note>
      <para>
-     <xref linkend="guc-restore-command"/> should return immediately
+     <xref linkend="guc-restore-command"/> and
+     <xref linkend="guc-restore-library"/> should return immediately
      if the file does not exist; the server will retry the command again if
      necessary.
     </para>
@@ -731,8 +736,9 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     If you're using a WAL archive, its size can be minimized using the <xref
-    linkend="guc-archive-cleanup-command"/> parameter to remove files that are no
-    longer required by the standby server.
+    linkend="guc-archive-cleanup-command"/> parameter or the
+    <xref linkend="guc-restore-library"/>'s archive cleanup callbacks to remove
+    files that are no longer required by the standby server.
     The <application>pg_archivecleanup</application> utility is designed specifically to
     be used with <varname>archive_cleanup_command</varname> in typical single-standby
     configurations, see <xref linkend="pgarchivecleanup"/>.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index e5d12f1d15..34a21c40a5 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -83,7 +83,7 @@
 #include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
-#include "restore/shell_restore.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
@@ -5178,18 +5178,19 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during
+	 * The callback is provided the archive file cutoff point for use during
 	 * log shipping replication.  All files earlier than this point can be
 	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_recovery_end_configured())
+	if (recovery_end_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_recovery_end(lastRestartPointFname);
+		RestoreCallbacks->recovery_end_cb(restore_module_state,
+										  lastRestartPointFname);
 	}
 
 	/*
@@ -7673,18 +7674,19 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Finally, execute archive-cleanup callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during
+	 * The callback is provided the archive file cutoff point for use during
 	 * log shipping replication.  All files earlier than this point can be
 	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_archive_cleanup_configured())
+	if (archive_cleanup_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_archive_cleanup(lastRestartPointFname);
+		RestoreCallbacks->archive_cleanup_cb(restore_module_state,
+											 lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index 5756f52124..3a05581495 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -23,14 +23,21 @@
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
 #include "common/archive.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/pgarch.h"
 #include "postmaster/startup.h"
 #include "replication/walsender.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
+#include "utils/memutils.h"
+
+char	   *restoreLibrary = "";
+const RestoreModuleCallbacks *RestoreCallbacks = NULL;
+RestoreModuleState *restore_module_state;
 
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
@@ -74,15 +81,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			if (!shell_restore_file_configured())
+			if (!restore_wal_segment_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			if (!shell_restore_file_configured())
+			if (!restore_timeline_history_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			if (!shell_restore_file_configured())
+			if (!timeline_history_exists_configured())
 				goto not_available;
 			break;
 	}
@@ -174,14 +181,17 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			ret = shell_restore_wal_segment(xlogfname, xlogpath,
-											lastRestartPointFname);
+			ret = RestoreCallbacks->restore_wal_segment_cb(restore_module_state,
+														   xlogfname, xlogpath,
+														   lastRestartPointFname);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->restore_timeline_history_cb(restore_module_state,
+																xlogfname, xlogpath);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->timeline_history_exists_cb(restore_module_state,
+															   xlogfname, xlogpath);
 			break;
 	}
 
@@ -189,6 +199,16 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 
 	if (ret)
 	{
+		/*
+		 * Some restore functions might not copy the file (e.g., checking
+		 * whether a timeline history file exists), but they can set a flag to
+		 * tell us if they do.  We only need to verify file existence if this
+		 * flag is enabled.
+		 */
+		if (archive_type == ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS &&
+			!restore_module_state->timeline_history_exists_cb_copies)
+			return true;
+
 		/*
 		 * command apparently succeeded, but let's make sure the file is
 		 * really there now and has the correct size.
@@ -630,3 +650,65 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the restore callbacks into our global RestoreCallbacks.  The
+ * caller is responsible for validating the combination of library/command
+ * parameters that are set (e.g., restore_command and restore_library cannot
+ * both be set).
+ */
+void
+LoadRestoreCallbacks(void)
+{
+	RestoreModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_restore_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RestoreModuleInit)
+			load_external_function(restoreLibrary, "_PG_restore_module_init",
+								   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("restore modules have to define the symbol "
+						"_PG_restore_module_init")));
+
+	RestoreCallbacks = (*init) ();
+
+	/* restore state should be freed before calling this function */
+	Assert(restore_module_state == NULL);
+	restore_module_state = (RestoreModuleState *)
+		MemoryContextAllocZero(TopMemoryContext,
+							   sizeof(RestoreModuleState));
+
+	if (RestoreCallbacks->startup_cb != NULL)
+		RestoreCallbacks->startup_cb(restore_module_state);
+}
+
+/*
+ * Call the shutdown callback of the loaded restore module, if defined.  Also,
+ * free the restore module state if it was allocated.
+ *
+ * Processes that load restore libraries should register this via
+ * before_shmem_exit().
+ */
+void
+call_restore_module_shutdown_cb(int code, Datum arg)
+{
+	if (RestoreCallbacks != NULL &&
+		RestoreCallbacks->shutdown_cb != NULL &&
+		restore_module_state != NULL)
+		RestoreCallbacks->shutdown_cb(restore_module_state);
+
+	if (restore_module_state != NULL)
+	{
+		pfree(restore_module_state);
+		restore_module_state = NULL;
+	}
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 07213b1897..1bf0d0ea11 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -51,6 +51,7 @@
 #include "replication/slot.h"
 #include "replication/slotsync.h"
 #include "replication/walreceiver.h"
+#include "restore/restore_module.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1117,18 +1118,21 @@ validateRecoveryParameters(void)
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			(!restore_wal_segment_configured() ||
+			 !restore_timeline_history_configured() ||
+			 !timeline_history_exists_configured()))
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
+					(errmsg("specified neither primary_conninfo nor restore_command nor a configured restore_library"),
 					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
 	}
 	else
 	{
-		if (recoveryRestoreCommand == NULL ||
-			strcmp(recoveryRestoreCommand, "") == 0)
+		if (!restore_wal_segment_configured() ||
+			!restore_timeline_history_configured() ||
+			!timeline_history_exists_configured())
 			ereport(FATAL,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("must specify restore_command when standby mode is not enabled")));
+					 errmsg("must specify restore_command or a configured restore_library when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 46197d56f8..47993aa35e 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -45,6 +45,7 @@
 #include "postmaster/bgwriter.h"
 #include "postmaster/interrupt.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -224,6 +225,25 @@ CheckpointerMain(void)
 												 ALLOCSET_DEFAULT_SIZES);
 	MemoryContextSwitchTo(checkpointer_context);
 
+	/*
+	 * Load restore_library so that we can use its archive-cleanup callback,
+	 * if one is defined.
+	 *
+	 * We also take this opportunity to set up the before_shmem_exit hook for
+	 * the shutdown callback and to check that only one of restore_library,
+	 * archive_cleanup_command is set.  Note that we emit a WARNING upon
+	 * detecting misconfigured parameters, and we clear the callbacks so that
+	 * the archive-cleanup functionality is skipped.  We judge this
+	 * functionality to not be important enough to require blocking
+	 * checkpoints or shutting down the server when the parameters are
+	 * misconfigured.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+										 archiveCleanupCommand, "archive_cleanup_command",
+										 WARNING))
+		LoadRestoreCallbacks();
+
 	/*
 	 * If an exception is encountered, processing resumes here.
 	 *
@@ -556,6 +576,10 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		bool		archive_cleanup_settings_changed = false;
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -571,6 +595,36 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		/*
+		 * If the archive-cleanup settings have changed, call the currently
+		 * loaded shutdown callback and clear all the restore callbacks.
+		 * There's presently no good way to unload a library besides
+		 * restarting the process, and there should be little harm in leaving
+		 * it around, so we just leave it loaded.
+		 */
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			archive_cleanup_settings_changed = true;
+			call_restore_module_shutdown_cb(0, (Datum) 0);
+			RestoreCallbacks = NULL;
+		}
+
+		/*
+		 * As in CheckpointerMain(), we only emit a WARNING if we detect that
+		 * both restore_library and archive_cleanup_command are set.  We do
+		 * this even if the archive-cleanup settings haven't changed to remind
+		 * the user about the misconfiguration.
+		 */
+		if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											 archiveCleanupCommand, "archive_cleanup_command",
+											 WARNING) &&
+			archive_cleanup_settings_changed)
+			LoadRestoreCallbacks();
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index e391bf6aa6..3cbd382e00 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -25,6 +25,7 @@
 #include "libpq/pqsignal.h"
 #include "miscadmin.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "storage/ipc.h"
 #include "storage/pmsignal.h"
 #include "storage/procsignal.h"
@@ -118,13 +119,17 @@ StartupProcShutdownHandler(SIGNAL_ARGS)
  * Re-read the config file.
  *
  * If one of the critical walreceiver options has changed, flag xlog.c
- * to restart it.
+ * to restart it.  Also, check for invalid combinations of the command/library
+ * parameters and reload the restore callbacks if necessary.
  */
 static void
 StartupRereadConfig(void)
 {
 	char	   *conninfo = pstrdup(PrimaryConnInfo);
 	char	   *slotname = pstrdup(PrimarySlotName);
+	char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+	char	   *prevRestoreCommand = pstrdup(recoveryRestoreCommand);
+	char	   *prevRecoveryEndCommand = pstrdup(recoveryEndCommand);
 	bool		tempSlot = wal_receiver_create_temp_slot;
 	bool		conninfoChanged;
 	bool		slotnameChanged;
@@ -146,6 +151,30 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	/*
+	 * If the restore settings have changed, call the currently loaded
+	 * shutdown callback and load the new callbacks.  There's presently no
+	 * good way to unload a library besides restarting the process, and there
+	 * should be little harm in leaving it around, so we just leave it loaded.
+	 */
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_restore_module_shutdown_cb(0, (Datum) 0);
+		LoadRestoreCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
@@ -255,6 +284,19 @@ StartupProcessMain(void)
 	 */
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	LoadRestoreCallbacks();
+
 	/*
 	 * Do what we came for.
 	 */
diff --git a/src/backend/restore/shell_restore.c b/src/backend/restore/shell_restore.c
index 42291625c1..be25f04044 100644
--- a/src/backend/restore/shell_restore.c
+++ b/src/backend/restore/shell_restore.c
@@ -3,7 +3,8 @@
  * shell_restore.c
  *
  * These restore functions use a user-specified shell command (e.g., the
- * restore_command GUC).
+ * restore_command GUC).  It is used as the default, but other modules may
+ * define their own restore logic.
  *
  * Copyright (c) 2024, PostgreSQL Global Development Group
  *
@@ -22,25 +23,76 @@
 #include "common/archive.h"
 #include "common/percentrepl.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static void shell_restore_startup(RestoreModuleState *state);
+static bool shell_restore_file_configured(RestoreModuleState *state);
 static bool shell_restore_file(const char *file, const char *path,
 							   const char *lastRestartPointFileName);
+static bool shell_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool shell_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool shell_archive_cleanup_configured(RestoreModuleState *state);
+static void shell_archive_cleanup(RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+static bool shell_recovery_end_configured(RestoreModuleState *state);
+static void shell_recovery_end(RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
 static void ExecuteRecoveryCommand(const char *command,
 								   const char *commandName,
 								   bool failOnSignal,
 								   uint32 wait_event_info,
 								   const char *lastRestartPointFileName);
 
+static const RestoreModuleCallbacks shell_restore_callbacks = {
+	.startup_cb = shell_restore_startup,
+	.restore_wal_segment_configured_cb = shell_restore_file_configured,
+	.restore_wal_segment_cb = shell_restore_wal_segment,
+	.restore_timeline_history_configured_cb = shell_restore_file_configured,
+	.restore_timeline_history_cb = shell_restore_timeline_history,
+	.timeline_history_exists_configured_cb = shell_restore_file_configured,
+	.timeline_history_exists_cb = shell_restore_timeline_history,
+	.archive_cleanup_configured_cb = shell_archive_cleanup_configured,
+	.archive_cleanup_cb = shell_archive_cleanup,
+	.recovery_end_configured_cb = shell_recovery_end_configured,
+	.recovery_end_cb = shell_recovery_end,
+	.shutdown_cb = NULL
+};
+
+/*
+ * shell_restore_init
+ *
+ * Returns the callbacks for restoring via shell.
+ */
+const RestoreModuleCallbacks *
+shell_restore_init(void)
+{
+	return &shell_restore_callbacks;
+}
+
+/*
+ * Indicates that our timeline_history_exists_cb only attempts to retrieve the
+ * file, so the caller should verify the file exists afterwards.
+ */
+static void
+shell_restore_startup(RestoreModuleState *state)
+{
+	state->timeline_history_exists_cb_copies = true;
+}
+
 /*
  * Attempt to execute a shell-based restore command to retrieve a WAL segment.
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_wal_segment(const char *file, const char *path,
+static bool
+shell_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
 						  const char *lastRestartPointFileName)
 {
 	return shell_restore_file(file, path, lastRestartPointFileName);
@@ -52,8 +104,9 @@ shell_restore_wal_segment(const char *file, const char *path,
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_timeline_history(const char *file, const char *path)
+static bool
+shell_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
 {
 	char		lastRestartPointFname[MAXPGPATH];
 
@@ -64,8 +117,8 @@ shell_restore_timeline_history(const char *file, const char *path)
 /*
  * Check whether restore_command is supplied.
  */
-bool
-shell_restore_file_configured(void)
+static bool
+shell_restore_file_configured(RestoreModuleState *state)
 {
 	return recoveryRestoreCommand[0] != '\0';
 }
@@ -152,8 +205,8 @@ shell_restore_file(const char *file, const char *path,
 /*
  * Check whether archive_cleanup_command is supplied.
  */
-bool
-shell_archive_cleanup_configured(void)
+static bool
+shell_archive_cleanup_configured(RestoreModuleState *state)
 {
 	return archiveCleanupCommand[0] != '\0';
 }
@@ -161,8 +214,9 @@ shell_archive_cleanup_configured(void)
 /*
  * Attempt to execute a shell-based archive cleanup command.
  */
-void
-shell_archive_cleanup(const char *lastRestartPointFileName)
+static void
+shell_archive_cleanup(RestoreModuleState *state,
+					  const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(archiveCleanupCommand, "archive_cleanup_command",
 						   false, WAIT_EVENT_ARCHIVE_CLEANUP_COMMAND,
@@ -172,8 +226,8 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 /*
  * Check whether recovery_end_command is supplied.
  */
-bool
-shell_recovery_end_configured(void)
+static bool
+shell_recovery_end_configured(RestoreModuleState *state)
 {
 	return recoveryEndCommand[0] != '\0';
 }
@@ -181,8 +235,9 @@ shell_recovery_end_configured(void)
 /*
  * Attempt to execute a shell-based end-of-recovery command.
  */
-void
-shell_recovery_end(const char *lastRestartPointFileName)
+static void
+shell_recovery_end(RestoreModuleState *state,
+				   const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(recoveryEndCommand, "recovery_end_command", true,
 						   WAIT_EVENT_RECOVERY_END_COMMAND,
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 57d9de4dd9..295cde9247 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -70,6 +70,7 @@
 #include "replication/slot.h"
 #include "replication/slotsync.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -3946,6 +3947,16 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"restore_library", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
+			gettext_noop("Sets the library that will be called for restore actions."),
+			NULL
+		},
+		&restoreLibrary,
+		"",
+		NULL, NULL, NULL
+	},
+
 	{
 		{"archive_cleanup_command", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
 			gettext_noop("Sets the shell command that will be executed at every restart point."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 2244ee52f7..5265857519 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -284,6 +284,7 @@
 				# placeholders: %p = path of file to restore
 				#               %f = file name only
 				# e.g. 'cp /mnt/server/archivedir/%f %p'
+#restore_library = ''		# library to use for restore actions
 #archive_cleanup_command = ''	# command to execute at every restartpoint
 #recovery_end_command = ''	# command to execute at completion of recovery
 
diff --git a/src/include/restore/restore_module.h b/src/include/restore/restore_module.h
new file mode 100644
index 0000000000..cc148e855a
--- /dev/null
+++ b/src/include/restore/restore_module.h
@@ -0,0 +1,143 @@
+/*-------------------------------------------------------------------------
+ *
+ * restore_module.h
+ *		Exports for restore modules.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * src/include/restore/restore_module.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef _RESTORE_MODULE_H
+#define _RESTORE_MODULE_H
+
+/*
+ * The value of the restore_library GUC.
+ */
+extern PGDLLIMPORT char *restoreLibrary;
+
+typedef struct RestoreModuleState
+{
+	/*
+	 * Private data pointer for use by a restore module.  This can be used to
+	 * store state for the module that will be passed to each of its
+	 * callbacks.
+	 */
+	void	   *private_data;
+
+	/*
+	 * Indicates whether the callback that checks for timeline existence
+	 * merely copies the file.  If set to true, the server will verify that
+	 * the timeline history file exists after the callback returns.
+	 */
+	bool		timeline_history_exists_cb_copies;
+} RestoreModuleState;
+
+/*
+ * Restore module callbacks
+ *
+ * These callback functions should be defined by restore libraries and returned
+ * via _PG_restore_module_init().  For more information about the purpose of
+ * each callback, refer to the restore modules documentation.
+ */
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state,
+									 const char *file, const char *path,
+									 const char *lastRestartPointFileName);
+typedef bool (*RestoreTimelineHistoryConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state,
+										  const char *file, const char *path);
+typedef bool (*TimelineHistoryExistsConfiguredCB) (RestoreModuleState *state);
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state,
+										 const char *file, const char *path);
+typedef bool (*ArchiveCleanupConfiguredCB) (RestoreModuleState *state);
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+typedef bool (*RecoveryEndConfiguredCB) (RestoreModuleState *state);
+typedef void (*RecoveryEndCB) (RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+
+typedef struct RestoreModuleCallbacks
+{
+	RestoreStartupCB startup_cb;
+	RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+	RestoreWalSegmentCB restore_wal_segment_cb;
+	RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+	RestoreTimelineHistoryCB restore_timeline_history_cb;
+	TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+	TimelineHistoryExistsCB timeline_history_exists_cb;
+	ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+	ArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndConfiguredCB recovery_end_configured_cb;
+	RecoveryEndCB recovery_end_cb;
+	RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+
+/*
+ * Type of the shared library symbol _PG_restore_module_init that is looked up
+ * when loading a restore library.
+ */
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+
+extern PGDLLEXPORT const RestoreModuleCallbacks *_PG_restore_module_init(void);
+
+extern void LoadRestoreCallbacks(void);
+extern void call_restore_module_shutdown_cb(int code, Datum arg);
+
+extern const RestoreModuleCallbacks *RestoreCallbacks;
+extern RestoreModuleState *restore_module_state;
+
+/*
+ * The following *_configured() functions are convenient wrappers around the
+ * *_configured_cb() callback functions with additional defensive NULL checks.
+ */
+
+static inline bool
+restore_wal_segment_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->restore_wal_segment_configured_cb != NULL &&
+		RestoreCallbacks->restore_wal_segment_cb != NULL &&
+		RestoreCallbacks->restore_wal_segment_configured_cb(restore_module_state);
+}
+
+static inline bool
+restore_timeline_history_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->restore_timeline_history_configured_cb != NULL &&
+		RestoreCallbacks->restore_timeline_history_cb != NULL &&
+		RestoreCallbacks->restore_timeline_history_configured_cb(restore_module_state);
+}
+
+static inline bool
+timeline_history_exists_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->timeline_history_exists_configured_cb != NULL &&
+		RestoreCallbacks->timeline_history_exists_cb != NULL &&
+		RestoreCallbacks->timeline_history_exists_configured_cb(restore_module_state);
+}
+
+static inline bool
+archive_cleanup_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->archive_cleanup_configured_cb != NULL &&
+		RestoreCallbacks->archive_cleanup_cb != NULL &&
+		RestoreCallbacks->archive_cleanup_configured_cb(restore_module_state);
+}
+
+static inline bool
+recovery_end_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->recovery_end_configured_cb != NULL &&
+		RestoreCallbacks->recovery_end_cb != NULL &&
+		RestoreCallbacks->recovery_end_configured_cb(restore_module_state);
+}
+
+#endif							/* _RESTORE_MODULE_H */
diff --git a/src/include/restore/shell_restore.h b/src/include/restore/shell_restore.h
index 28b5b7bc92..6352623866 100644
--- a/src/include/restore/shell_restore.h
+++ b/src/include/restore/shell_restore.h
@@ -12,15 +12,13 @@
 #ifndef _SHELL_RESTORE_H
 #define _SHELL_RESTORE_H
 
-extern bool shell_restore_file_configured(void);
-extern bool shell_restore_wal_segment(const char *file, const char *path,
-									  const char *lastRestartPointFileName);
-extern bool shell_restore_timeline_history(const char *file, const char *path);
+#include "restore/restore_module.h"
 
-extern bool shell_archive_cleanup_configured(void);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-
-extern bool shell_recovery_end_configured(void);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Since the logic for restoring via shell commands is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern const RestoreModuleCallbacks *shell_restore_init(void);
 
 #endif							/* _SHELL_RESTORE_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 2f975ac53a..8e190a0c79 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2388,6 +2388,8 @@ ResourceOwnerDesc
 ResourceReleaseCallback
 ResourceReleaseCallbackItem
 ResourceReleasePhase
+RestoreModuleCallbacks
+RestoreModuleState
 RestoreOptions
 RestorePass
 RestrictInfo
-- 
2.25.1


--oyUTqETQ0mS9luUI--





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

* [PATCH v20 5/5] introduce restore_library
@ 2023-02-16 06:06  Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 28+ messages in thread

From: Nathan Bossart @ 2023-02-16 06:06 UTC (permalink / raw)

---
 contrib/basic_archive/Makefile                |   2 +
 contrib/basic_archive/basic_archive.c         | 153 +++++++-
 contrib/basic_archive/meson.build             |   5 +
 contrib/basic_archive/t/001_restore.pl        |  44 +++
 doc/src/sgml/archive-and-restore-modules.sgml | 356 +++++++++++++++++-
 doc/src/sgml/backup.sgml                      |  40 +-
 doc/src/sgml/basic-archive.sgml               |  31 +-
 doc/src/sgml/config.sgml                      |  53 ++-
 doc/src/sgml/high-availability.sgml           |  18 +-
 src/backend/access/transam/xlog.c             |  20 +-
 src/backend/access/transam/xlogarchive.c      |  96 ++++-
 src/backend/access/transam/xlogrecovery.c     |  14 +-
 src/backend/postmaster/checkpointer.c         |  54 +++
 src/backend/postmaster/startup.c              |  44 ++-
 src/backend/restore/shell_restore.c           |  85 ++++-
 src/backend/utils/misc/guc_tables.c           |  11 +
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/restore/restore_module.h          | 143 +++++++
 src/include/restore/shell_restore.h           |  16 +-
 src/tools/pgindent/typedefs.list              |   2 +
 20 files changed, 1104 insertions(+), 84 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl
 create mode 100644 src/include/restore/restore_module.h

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 100ed81f12..e61f7d676f 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -10,6 +10,8 @@ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.c
 # typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
 
+TAP_TESTS = 1
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index 6b102e9072..e0d8dce364 100644
--- a/contrib/basic_archive/basic_archive.c
+++ b/contrib/basic_archive/basic_archive.c
@@ -17,6 +17,11 @@
  * a file is successfully archived and then the system crashes before
  * a durable record of the success has been made.
  *
+ * This file also demonstrates a basic restore library implementation that
+ * is roughly equivalent to the following shell command:
+ *
+ *		cp /path/to/archivedir/%f %p
+ *
  * Copyright (c) 2022-2024, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
@@ -33,6 +38,7 @@
 #include "archive/archive_module.h"
 #include "common/int.h"
 #include "miscadmin.h"
+#include "restore/restore_module.h"
 #include "storage/copydir.h"
 #include "storage/fd.h"
 #include "utils/guc.h"
@@ -54,6 +60,16 @@ static void basic_archive_file_internal(const char *file, const char *path);
 static bool check_archive_directory(char **newval, void **extra, GucSource source);
 static bool compare_files(const char *file1, const char *file2);
 static void basic_archive_shutdown(ArchiveModuleState *state);
+static bool basic_restore_configured(RestoreModuleState *state);
+static bool basic_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool basic_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool basic_restore_file(const char *file, const char *path);
+static bool basic_restore_timeline_history_exists(RestoreModuleState *state,
+												  const char *file,
+												  const char *path);
 
 static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.startup_cb = basic_archive_startup,
@@ -62,6 +78,21 @@ static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.shutdown_cb = basic_archive_shutdown
 };
 
+static const RestoreModuleCallbacks basic_restore_callbacks = {
+	.startup_cb = NULL,
+	.restore_wal_segment_configured_cb = basic_restore_configured,
+	.restore_wal_segment_cb = basic_restore_wal_segment,
+	.restore_timeline_history_configured_cb = basic_restore_configured,
+	.restore_timeline_history_cb = basic_restore_timeline_history,
+	.timeline_history_exists_configured_cb = basic_restore_configured,
+	.timeline_history_exists_cb = basic_restore_timeline_history_exists,
+	.archive_cleanup_configured_cb = NULL,
+	.archive_cleanup_cb = NULL,
+	.recovery_end_configured_cb = NULL,
+	.recovery_end_cb = NULL,
+	.shutdown_cb = NULL
+};
+
 /*
  * _PG_init
  *
@@ -71,7 +102,7 @@ void
 _PG_init(void)
 {
 	DefineCustomStringVariable("basic_archive.archive_directory",
-							   gettext_noop("Archive file destination directory."),
+							   gettext_noop("Archive file source/destination directory."),
 							   NULL,
 							   &archive_directory,
 							   "",
@@ -93,6 +124,17 @@ _PG_archive_module_init(void)
 	return &basic_archive_callbacks;
 }
 
+/*
+ * _PG_restore_module_init
+ *
+ * Returns the module's restore callbacks.
+ */
+const RestoreModuleCallbacks *
+_PG_restore_module_init(void)
+{
+	return &basic_restore_callbacks;
+}
+
 /*
  * basic_archive_startup
  *
@@ -431,3 +473,112 @@ basic_archive_shutdown(ArchiveModuleState *state)
 	pfree(data);
 	state->private_data = NULL;
 }
+
+/*
+ * basic_restore_configured
+ *
+ * Checks that archive_directory is not blank.
+ */
+static bool
+basic_restore_configured(RestoreModuleState *state)
+{
+	return archive_directory != NULL && archive_directory[0] != '\0';
+}
+
+/*
+ * basic_restore_wal_segment
+ *
+ * Retrieves one archived WAL segment.
+ */
+static bool
+basic_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
+						  const char *lastRestartPointFileName)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_timeline_history
+ *
+ * Retrieves one timeline history file.
+ */
+static bool
+basic_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file.
+ */
+static bool
+basic_restore_file(const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive",
+					file)));
+
+	/*
+	 * If the file doesn't exist, return false to indicate that there are no
+	 * more files to restore.
+	 */
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	copy_file(archive_path, path);
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive",
+					file)));
+	return true;
+}
+
+/*
+ * basic_restore_timeline_history_exists
+ *
+ * Check whether a timeline history file is present in the archive directory.
+ */
+static bool
+basic_restore_timeline_history_exists(RestoreModuleState *state,
+									  const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("checking existence of \"%s\" via basic_archive",
+					file)));
+
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	ereport(DEBUG1,
+			(errmsg("verified existence of \"%s\" via basic_archive",
+					file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index cc2f62bf36..8e36fa7ed6 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -31,4 +31,9 @@ tests += {
     # typical installcheck users do not have (e.g. buildfarm clients).
     'runningcheck': false,
   },
+  'tap': {
+    'tests': [
+      't/001_restore.pl',
+    ],
+  },
 }
diff --git a/contrib/basic_archive/t/001_restore.pl b/contrib/basic_archive/t/001_restore.pl
new file mode 100644
index 0000000000..6c01f736cf
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,44 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# start a node
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init(has_archiving => 1, allows_streaming => 1);
+my $archive_dir = $node->archive_dir;
+$archive_dir =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
+$node->append_conf('postgresql.conf', "archive_command = ''");
+$node->append_conf('postgresql.conf', "archive_library = 'basic_archive'");
+$node->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$node->start;
+
+# backup the node
+my $backup = 'backup';
+$node->backup($backup);
+
+# generate some new WAL files
+$node->safe_psql('postgres', "CREATE TABLE test (a INT);");
+$node->safe_psql('postgres', "SELECT pg_switch_wal();");
+$node->safe_psql('postgres', "INSERT INTO test VALUES (1);");
+
+# shut down the node (this should archive all WAL files)
+$node->stop;
+
+# restore from the backup
+my $restore = PostgreSQL::Test::Cluster->new('restore');
+$restore->init_from_backup($node, $backup, has_restoring => 1, standby => 0);
+$restore->append_conf('postgresql.conf', "restore_command = ''");
+$restore->append_conf('postgresql.conf', "restore_library = 'basic_archive'");
+$restore->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$restore->start;
+
+# ensure post-backup WAL is replayed
+my $result = $restore->poll_query_until("postgres", "SELECT count(*) = 1 FROM test;");
+is($result, "1", "check restore content");
+
+done_testing();
diff --git a/doc/src/sgml/archive-and-restore-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml
index a52d15082d..15be548a73 100644
--- a/doc/src/sgml/archive-and-restore-modules.sgml
+++ b/doc/src/sgml/archive-and-restore-modules.sgml
@@ -8,15 +8,17 @@
 
  <para>
   PostgreSQL provides infrastructure to create custom modules for continuous
-  archiving (see <xref linkend="continuous-archiving"/>).  While archiving via
-  a shell command (i.e., <xref linkend="guc-archive-command"/>) is much
-  simpler, a custom archive module will often be considerably more robust and
-  performant.
+  archiving and recovery (see <xref linkend="continuous-archiving"/>).  While a
+  shell command (i.e., <xref linkend="guc-archive-command"/>,
+  <xref linkend="guc-restore-command"/>) is much simpler, a custom module will
+  often be considerably more robust and performant.
  </para>
 
  <para>
   The <filename>contrib/basic_archive</filename> module contains a working
-  example, which demonstrates some useful techniques.
+  example, which demonstrates some useful techniques.  As demonstrated in
+  <filename>basic_archive</filename> a single module may serve as both an
+  archive module and a restore module.
  </para>
 
  <sect1 id="archive-modules">
@@ -178,4 +180,348 @@ typedef void (*ArchiveShutdownCB) (ArchiveModuleState *state);
    </sect3>
   </sect2>
  </sect1>
+
+ <sect1 id="restore-modules">
+  <title>Restore Modules</title>
+  <indexterm zone="restore-modules">
+   <primary>Restore Modules</primary>
+  </indexterm>
+
+  <para>
+   When a custom <xref linkend="guc-restore-library"/> is configured,
+   PostgreSQL will use the module for restore actions.  It is
+   ultimately up to the module to decide how to accomplish each task,
+   but some recommendations are listed at
+   <xref linkend="backup-pitr-recovery"/>.
+  </para>
+
+  <para>
+   Restore modules must at least consist of an initialization function
+   (see <xref linkend="restore-module-init"/>) and the required callbacks (see
+   <xref linkend="restore-module-callbacks"/>).  However, restore modules are
+   also permitted to do much more (e.g., declare GUCs and register background
+   workers).
+  </para>
+
+  <sect2 id="restore-module-init">
+   <title>Initialization Functions</title>
+   <indexterm zone="restore-module-init">
+    <primary>_PG_restore_module_init</primary>
+   </indexterm>
+
+   <para>
+    An restore library is loaded by dynamically loading a shared library with
+    the <xref linkend="guc-restore-library"/>'s name as the library base name.
+    The normal library search path is used to locate the library.  To provide
+    the required restore module callbacks and to indicate that the library is
+    actually a restore module, it needs to provide a function named
+    <function>_PG_restore_module_init</function>.  The result of the function
+    must be a pointer to a struct of type
+    <structname>RestoreModuleCallbacks</structname>, which contains everything
+    that the core code needs to know how to make use of the restore module.
+    The return value needs to be of server lifetime, which is typically
+    achieved by defining it as a <literal>static const</literal> variable in
+    global scope.
+
+<programlisting>
+typedef struct RestoreModuleCallbacks
+{
+    RestoreStartupCB startup_cb;
+    RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+    RestoreWalSegmentCB restore_wal_segment_cb;
+    RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+    RestoreTimelineHistoryCB restore_timeline_history_cb;
+    TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+    TimelineHistoryExistsCB timeline_history_exists_cb;
+    ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+    ArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndConfiguredCB recovery_end_configured_cb;
+    RecoveryEndCB recovery_end_cb;
+    RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+</programlisting>
+
+    The <function>restore_wal_segment_configured_cb</function>,
+    <function>restore_wal_segment_cb</function>,
+    <function>restore_timeline_history_configured_cb</function>,
+    <function>restore_timeline_history_cb</function>,
+    <function>timeline_history_exists_configured_cb</function>, and
+    <function>timeline_history_exists_cb</function> callbacks are required for
+    archive recovery but optional for streaming replication.  The others are
+    always optional.
+   </para>
+  </sect2>
+
+  <sect2 id="restore-module-callbacks">
+   <title>Restore Module Callbacks</title>
+
+   <para>
+    The restore callbacks define the actual behavior of the module.  The server
+    will call them as required to execute restore actions.
+   </para>
+
+   <sect3 id="restore-module-startup">
+    <title>Startup Callback</title>
+
+    <para>
+     The <function>startup_cb</function> callback is called shortly after the
+     module is loaded.  This callback can be used to perform any additional
+     initialization required.  If the restore module has state, it can use
+     <structfield>state->private_data</structfield> to store it.
+
+<programlisting>
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+</programlisting>
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment-configured">
+    <title>Restore WAL Segment Configured Callback</title>
+    <para>
+     The <function>restore_wal_segment_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to retrieve WAL files (e.g., its configuration parameters
+     are set to valid values).  If no
+     <function>restore_wal_segment_configured_cb</function> is defined, the
+     server always assumes the module is not configured to retrieve WAL files.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve WAL files
+     by calling the <function>restore_wal_segment_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_wal_segment_cb</function> callback to retrieve WAL
+     files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment">
+    <title>Restore WAL Segment Callback</title>
+    <para>
+     The <function>restore_wal_segment_cb</function> callback is called to
+     retrieve a single archived segment of the WAL file series for archive
+     recovery or streaming replication.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state, const char *file, const char *path, const char *lastRestartPointFileName);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point.  That is the earliest
+     file that must be kept to allow a restore to be restartable, so this
+     information can be used to truncate the archive to just the minimum
+     required to support restarting from the current restore.
+     <replaceable>lastRestartPointFileName</replaceable> is typically only used
+     by warm-standby configurations (see <xref linkend="warm-standby"/>).  Note
+     that if multiple standby servers are restoring from the same archive
+     directory, you will need to ensure that you do not delete WAL files until
+     they are no longer needed by any of the servers.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history-configured">
+    <title>Restore Timeline History Configured Callback</title>
+    <para>
+     The <function>restore_timeline_history_configured_cb</function> callback
+     is called to determine whether the module is fully configured and ready to
+     accept requests to retrieve timeline history files (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>restore_timeline_history_configured_cb</function> is defined,
+     the server always assumes the module is not configured to retrieve
+     timeline history files.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve timeline
+     history files by calling the
+     <function>restore_timeline_history_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_timeline_history_cb</function> callback to retrieve
+     timeline history files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history">
+    <title>Restore Timeline History Callback</title>
+    <para>
+     The <function>restore_timeline_history_cb</function> callback is called to
+     retrieve a single archived timeline history file for archive recovery or
+     streaming replication.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists-configured">
+    <title>Timeline History Exists Configured Callback</title>
+    <para>
+     The <function>timeline_history_exists_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to determine whether a timeline history file exists in the
+     archives (e.g., its configuration parameters are set to valid values).  If
+     no <function>timeline_history_exists_configured_cb</function> is defined,
+     the server always assumes the module is not configured to check whether
+     certain timeline history files exist.
+
+<programlisting>
+typedef bool (*TimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will check whether
+     certain timeline history files are present in the archives by calling the
+     <function>timeline_history_exists_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>timeline_history_exists_cb</function> callback to check if
+     timeline history files exist in the archives.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists">
+    <title>Timeline History Exists Callback</title>
+    <para>
+     The <function>timeline_history_exists_cb</function> callback is called to
+     check whether a timeline history file is present in the archives.
+
+<programlisting>
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file is
+     present in the archives.  If the file is not available in the archives,
+     the callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the timeline history file.
+    </para>
+
+    <para>
+     Some restore modules might not have a dedicated way to determine whether a
+     timeline history file exists.  For example,
+     <varname>restore_command</varname> only tells the server how to retrieve
+     files.  In this case, the <function>timeline_history_exists_cb</function>
+     callback should copy the file to <replaceable>path</replaceable>, which
+     contains the destination's relative path (including the file name), and
+     the restore module should set
+     <structfield>state->timeline_history_exists_cb_copies</structfield> to
+     <literal>true</literal>.  It is recommended to set this flag in the
+     module's <function>startup_cb</function> callback.  This flag tells the
+     server that it should verify that the file was successfully retrieved
+     after invoking this callback.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup-configured">
+    <title>Archive Cleanup Configured Callback</title>
+    <para>
+     The <function>archive_cleanup_configured_cb</function> callback is called
+     to determine whether the module is fully configured and ready to accept
+     requests to remove old WAL files from the archives (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>archive_cleanup_configured_cb</function> is defined, the server
+     always assumes the module is not configured to remove old WAL files.
+
+<programlisting>
+typedef bool (*ArchiveCleanupConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will remove old WAL
+     files by calling the <function>archive_cleanup_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>archive_cleanup_cb</function> callback to remove old WAL files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup">
+    <title>Archive Cleanup Callback</title>
+    <para>
+     The <function>archive_cleanup_cb</function> callback is called at every
+     restart point by the checkpointer process and is intended to provide a
+     mechanism for cleaning up old archived WAL files that are no longer
+     needed by the standby server.
+
+<programlisting>
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the
+     name of the file that includes the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end-configured">
+    <title>Recovery End Configured Callback</title>
+    <para>
+     The <function>recovery_end_configured_cb</function> callback is called to
+     determine whether the module is fully configured and ready to execute its
+     <function>recovery_end_cb</function> callback once at the end of recovery
+     (e.g., its configuration parameters are set to valid values).  If no
+     <function>recovery_end_configured_cb</function> is defined, the server
+     always assumes the module is not configured to execute its
+     <function>recovery_end_cb</function> callback.
+
+<programlisting>
+typedef bool (*RecoveryEndConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will execute the
+     <function>recovery_end_cb</function> callback once at the end of recovery.
+     If <literal>false</literal> is returned, the server will not use the
+     <function>recovery_end_cb</function> callback at the end of recovery.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end">
+    <title>Recovery End Callback</title>
+    <para>
+     The <function>recovery_end_cb</function> callback is called once at the
+     end of recovery and is intended to provide a mechanism for cleanup
+     following the end of replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-shutdown">
+    <title>Shutdown Callback</title>
+    <para>
+     The <function>shutdown_cb</function> callback is called when a process
+     that has loaded the restore module exits (e.g., after an error) or the
+     value of <xref linkend="guc-restore-library"/> changes.  If no
+     <function>shutdown_cb</function> is defined, no special action is taken in
+     these situations.  If the restore module has state, this callback should
+     free it to avoid leaks.
+
+<programlisting>
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+ </programlisting>
+    </para>
+   </sect3>
+  </sect2>
+ </sect1>
 </chapter>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index b3468eea3c..e1643f92e0 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1261,9 +1261,26 @@ SELECT * FROM pg_backup_stop(wait_for_archive => true);
    <para>
     The key part of all this is to set up a recovery configuration that
     describes how you want to recover and how far the recovery should
-    run.  The one thing that you absolutely must specify is the <varname>restore_command</varname>,
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>,
     which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    WAL file segments.
+   </para>
+
+   <para>
+    Like the <varname>archive_library</varname> parameter,
+    <varname>restore_library</varname> is a shared library.  Since such
+    libraries are written in <literal>C</literal>, creating your own may
+    require considerably more effort than writing a shell command.  However,
+    restore modules can be more performance than restoring via shell, and they
+    will have access to many useful server resources.  For more information
+    about creating a <varname>restore_library</varname>, see
+    <xref linkend="restore-modules"/>.
+   </para>
+
+   <para>
+    Like the <varname>archive_command</varname>,
+    <varname>restore_command</varname> is
     a shell command string.  It can contain <literal>%f</literal>, which is
     replaced by the name of the desired WAL file, and <literal>%p</literal>,
     which is replaced by the path name to copy the WAL file to.
@@ -1282,14 +1299,20 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
    </para>
 
    <para>
-    It is important that the command return nonzero exit status on failure.
-    The command <emphasis>will</emphasis> be called requesting files that are not
-    present in the archive; it must return nonzero when so asked.  This is not
-    an error condition.  An exception is that if the command was terminated by
+    It is important that the <varname>restore_command</varname> return nonzero
+    exit status on failure, or, if you are using a
+    <varname>restore_library</varname>, that the restore callbacks return
+    <literal>false</literal> on failure.  The command or library
+    <emphasis>will</emphasis> be called requesting files that are not
+    present in the archive; it must fail when so asked.  This is not
+    an error condition.  An exception is that if the
+    <varname>restore_command</varname> was terminated by
     a signal (other than <systemitem>SIGTERM</systemitem>, which is used as
     part of a database server shutdown) or an error by the shell (such as
     command not found), then recovery will abort and the server will not start
-    up.
+    up.  Likewise, if the restore callbacks provided by the
+    <varname>restore_library</varname> emit an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1313,7 +1336,8 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
     close as possible given the available WAL segments).  Therefore, a normal
     recovery will end with a <quote>file not found</quote> message, the exact text
     of the error message depending upon your choice of
-    <varname>restore_command</varname>.  You may also see an error message
+    <varname>restore_command</varname> or <varname>restore_library</varname>.
+    You may also see an error message
     at the start of recovery for a file named something like
     <filename>00000001.history</filename>.  This is also normal and does not
     indicate a problem in simple recovery situations; see
diff --git a/doc/src/sgml/basic-archive.sgml b/doc/src/sgml/basic-archive.sgml
index b4d43ced20..f3a5a502bd 100644
--- a/doc/src/sgml/basic-archive.sgml
+++ b/doc/src/sgml/basic-archive.sgml
@@ -8,17 +8,20 @@
  </indexterm>
 
  <para>
-  <filename>basic_archive</filename> is an example of an archive module.  This
-  module copies completed WAL segment files to the specified directory.  This
-  may not be especially useful, but it can serve as a starting point for
-  developing your own archive module.  For more information about archive
-  modules, see <xref linkend="archive-modules"/>.
+  <filename>basic_archive</filename> is an example of an archive and restore
+  module.  This module copies completed WAL segment files to or from the
+  specified directory.  This may not be especially useful, but it can serve as
+  a starting point for developing your own archive and restore modules.  For
+  more information about archive and restore modules, see\
+  <xref linkend="archive-and-restore-modules"/>.
  </para>
 
  <para>
-  In order to function, this module must be loaded via
+  For use as an archive module, this module must be loaded via
   <xref linkend="guc-archive-library"/>, and <xref linkend="guc-archive-mode"/>
-  must be enabled.
+  must be enabled.  For use as a restore module, this module must be loaded
+  via <xref linkend="guc-restore-library"/>, and recovery must be enabled (see
+  <xref linkend="runtime-config-wal-archive-recovery"/>).
  </para>
 
  <sect2 id="basic-archive-configuration-parameters">
@@ -34,11 +37,12 @@
     </term>
     <listitem>
      <para>
-      The directory where the server should copy WAL segment files.  This
-      directory must already exist.  The default is an empty string, which
-      effectively halts WAL archiving, but if <xref linkend="guc-archive-mode"/>
-      is enabled, the server will accumulate WAL segment files in the
-      expectation that a value will soon be provided.
+      The directory where the server should copy WAL segment files to or from.
+      This directory must already exist.  The default is an empty string,
+      which, when used for archiving, effectively halts WAL archival, but if
+      <xref linkend="guc-archive-mode"/> is enabled, the server will accumulate
+      WAL segment files in the expectation that a value will soon be provided.
+      When an empty string is used for recovery, restore will fail.
      </para>
     </listitem>
    </varlistentry>
@@ -46,7 +50,7 @@
 
   <para>
    These parameters must be set in <filename>postgresql.conf</filename>.
-   Typical usage might be:
+   Typical usage as an archive module might be:
   </para>
 
 <programlisting>
@@ -61,6 +65,7 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
+   When <filename>basic_archive</filename> is used as an archive module,
    Server crashes may leave temporary files with the prefix
    <filename>archtemp</filename> in the archive directory.  It is recommended to
    delete such files before restarting the server after a crash.  It is safe to
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 65a6e6c408..9a2570f87f 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3906,7 +3906,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
@@ -3934,7 +3935,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,
@@ -3969,7 +3971,42 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
 
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>restore_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-restore-library" xreflabel="restore_library">
+      <term><varname>restore_library</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>restore_library</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        The library to use for restore actions, including retrieving archived
+        files and executing tasks at restartpoints and at recovery end.  Either
+        <xref linkend="guc-restore-command"/> or
+        <varname>restore_library</varname> is required for archive recovery,
+        but optional for streaming replication.  If this parameter is set to an
+        empty string (the default), restoring via shell is enabled, and
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, and
+        <varname>recovery_end_command</varname> are used.  If both
+        <varname>restore_library</varname> and any of
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, or
+        <varname>recovery_end_command</varname> are set, an error will be
+        raised.  Otherwise, the specified shared library is used for restoring.
+        For more information, see <xref linkend="restore-modules"/>.
+       </para>
+
+       <para>
+        This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
        </para>
       </listitem>
      </varlistentry>
@@ -4014,7 +4051,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>
@@ -4043,7 +4083,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
        </para>
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>recovery_end_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index b48209fc2f..a86a9fe93a 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -627,7 +627,8 @@ protocol to make nodes agree on a serializable transactional order.
    <para>
     In standby mode, the server continuously applies WAL received from the
     primary server. The standby server can read WAL from a WAL archive
-    (see <xref linkend="guc-restore-command"/>) or directly from the primary
+    (see <xref linkend="guc-restore-command"/> and
+    <xref linkend="guc-restore-library"/>) or directly from the primary
     over a TCP connection (streaming replication). The standby server will
     also attempt to restore any WAL found in the standby cluster's
     <filename>pg_wal</filename> directory. That typically happens after a server
@@ -638,8 +639,10 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     At startup, the standby begins by restoring all WAL available in the
-    archive location, calling <varname>restore_command</varname>. Once it
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s callbacks.  Once it
     reaches the end of WAL available there and <varname>restore_command</varname>
+    or <varname>restore_library</varname>
     fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
     If that fails, and streaming replication has been configured, the
     standby tries to connect to the primary server and start streaming WAL
@@ -698,7 +701,8 @@ protocol to make nodes agree on a serializable transactional order.
     server (see <xref linkend="backup-pitr-recovery"/>). Create a file
     <link linkend="file-standby-signal"><filename>standby.signal</filename></link><indexterm><primary>standby.signal</primary></indexterm>
     in the standby's cluster data
-    directory. Set <xref linkend="guc-restore-command"/> to a simple command to copy files from
+    directory. Set <xref linkend="guc-restore-command"/> or
+    <xref linkend="guc-restore-library"/> to copy files from
     the WAL archive. If you plan to have multiple standby servers for high
     availability purposes, make sure that <varname>recovery_target_timeline</varname> is set to
     <literal>latest</literal> (the default), to make the standby server follow the timeline change
@@ -707,7 +711,8 @@ protocol to make nodes agree on a serializable transactional order.
 
    <note>
      <para>
-     <xref linkend="guc-restore-command"/> should return immediately
+     <xref linkend="guc-restore-command"/> and
+     <xref linkend="guc-restore-library"/> should return immediately
      if the file does not exist; the server will retry the command again if
      necessary.
     </para>
@@ -731,8 +736,9 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     If you're using a WAL archive, its size can be minimized using the <xref
-    linkend="guc-archive-cleanup-command"/> parameter to remove files that are no
-    longer required by the standby server.
+    linkend="guc-archive-cleanup-command"/> parameter or the
+    <xref linkend="guc-restore-library"/>'s archive cleanup callbacks to remove
+    files that are no longer required by the standby server.
     The <application>pg_archivecleanup</application> utility is designed specifically to
     be used with <varname>archive_cleanup_command</varname> in typical single-standby
     configurations, see <xref linkend="pgarchivecleanup"/>.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index e5d12f1d15..34a21c40a5 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -83,7 +83,7 @@
 #include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
-#include "restore/shell_restore.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
@@ -5178,18 +5178,19 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during
+	 * The callback is provided the archive file cutoff point for use during
 	 * log shipping replication.  All files earlier than this point can be
 	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_recovery_end_configured())
+	if (recovery_end_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_recovery_end(lastRestartPointFname);
+		RestoreCallbacks->recovery_end_cb(restore_module_state,
+										  lastRestartPointFname);
 	}
 
 	/*
@@ -7673,18 +7674,19 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Finally, execute archive-cleanup callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during
+	 * The callback is provided the archive file cutoff point for use during
 	 * log shipping replication.  All files earlier than this point can be
 	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_archive_cleanup_configured())
+	if (archive_cleanup_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_archive_cleanup(lastRestartPointFname);
+		RestoreCallbacks->archive_cleanup_cb(restore_module_state,
+											 lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index 5756f52124..3a05581495 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -23,14 +23,21 @@
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
 #include "common/archive.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/pgarch.h"
 #include "postmaster/startup.h"
 #include "replication/walsender.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
+#include "utils/memutils.h"
+
+char	   *restoreLibrary = "";
+const RestoreModuleCallbacks *RestoreCallbacks = NULL;
+RestoreModuleState *restore_module_state;
 
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
@@ -74,15 +81,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			if (!shell_restore_file_configured())
+			if (!restore_wal_segment_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			if (!shell_restore_file_configured())
+			if (!restore_timeline_history_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			if (!shell_restore_file_configured())
+			if (!timeline_history_exists_configured())
 				goto not_available;
 			break;
 	}
@@ -174,14 +181,17 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			ret = shell_restore_wal_segment(xlogfname, xlogpath,
-											lastRestartPointFname);
+			ret = RestoreCallbacks->restore_wal_segment_cb(restore_module_state,
+														   xlogfname, xlogpath,
+														   lastRestartPointFname);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->restore_timeline_history_cb(restore_module_state,
+																xlogfname, xlogpath);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->timeline_history_exists_cb(restore_module_state,
+															   xlogfname, xlogpath);
 			break;
 	}
 
@@ -189,6 +199,16 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 
 	if (ret)
 	{
+		/*
+		 * Some restore functions might not copy the file (e.g., checking
+		 * whether a timeline history file exists), but they can set a flag to
+		 * tell us if they do.  We only need to verify file existence if this
+		 * flag is enabled.
+		 */
+		if (archive_type == ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS &&
+			!restore_module_state->timeline_history_exists_cb_copies)
+			return true;
+
 		/*
 		 * command apparently succeeded, but let's make sure the file is
 		 * really there now and has the correct size.
@@ -630,3 +650,65 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the restore callbacks into our global RestoreCallbacks.  The
+ * caller is responsible for validating the combination of library/command
+ * parameters that are set (e.g., restore_command and restore_library cannot
+ * both be set).
+ */
+void
+LoadRestoreCallbacks(void)
+{
+	RestoreModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_restore_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RestoreModuleInit)
+			load_external_function(restoreLibrary, "_PG_restore_module_init",
+								   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("restore modules have to define the symbol "
+						"_PG_restore_module_init")));
+
+	RestoreCallbacks = (*init) ();
+
+	/* restore state should be freed before calling this function */
+	Assert(restore_module_state == NULL);
+	restore_module_state = (RestoreModuleState *)
+		MemoryContextAllocZero(TopMemoryContext,
+							   sizeof(RestoreModuleState));
+
+	if (RestoreCallbacks->startup_cb != NULL)
+		RestoreCallbacks->startup_cb(restore_module_state);
+}
+
+/*
+ * Call the shutdown callback of the loaded restore module, if defined.  Also,
+ * free the restore module state if it was allocated.
+ *
+ * Processes that load restore libraries should register this via
+ * before_shmem_exit().
+ */
+void
+call_restore_module_shutdown_cb(int code, Datum arg)
+{
+	if (RestoreCallbacks != NULL &&
+		RestoreCallbacks->shutdown_cb != NULL &&
+		restore_module_state != NULL)
+		RestoreCallbacks->shutdown_cb(restore_module_state);
+
+	if (restore_module_state != NULL)
+	{
+		pfree(restore_module_state);
+		restore_module_state = NULL;
+	}
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 07213b1897..1bf0d0ea11 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -51,6 +51,7 @@
 #include "replication/slot.h"
 #include "replication/slotsync.h"
 #include "replication/walreceiver.h"
+#include "restore/restore_module.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1117,18 +1118,21 @@ validateRecoveryParameters(void)
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			(!restore_wal_segment_configured() ||
+			 !restore_timeline_history_configured() ||
+			 !timeline_history_exists_configured()))
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
+					(errmsg("specified neither primary_conninfo nor restore_command nor a configured restore_library"),
 					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
 	}
 	else
 	{
-		if (recoveryRestoreCommand == NULL ||
-			strcmp(recoveryRestoreCommand, "") == 0)
+		if (!restore_wal_segment_configured() ||
+			!restore_timeline_history_configured() ||
+			!timeline_history_exists_configured())
 			ereport(FATAL,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("must specify restore_command when standby mode is not enabled")));
+					 errmsg("must specify restore_command or a configured restore_library when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 8ef600ae72..c31d0f3bd2 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -46,6 +46,7 @@
 #include "postmaster/bgwriter.h"
 #include "postmaster/interrupt.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -230,6 +231,25 @@ CheckpointerMain(char *startup_data, size_t startup_data_len)
 												 ALLOCSET_DEFAULT_SIZES);
 	MemoryContextSwitchTo(checkpointer_context);
 
+	/*
+	 * Load restore_library so that we can use its archive-cleanup callback,
+	 * if one is defined.
+	 *
+	 * We also take this opportunity to set up the before_shmem_exit hook for
+	 * the shutdown callback and to check that only one of restore_library,
+	 * archive_cleanup_command is set.  Note that we emit a WARNING upon
+	 * detecting misconfigured parameters, and we clear the callbacks so that
+	 * the archive-cleanup functionality is skipped.  We judge this
+	 * functionality to not be important enough to require blocking
+	 * checkpoints or shutting down the server when the parameters are
+	 * misconfigured.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+										 archiveCleanupCommand, "archive_cleanup_command",
+										 WARNING))
+		LoadRestoreCallbacks();
+
 	/*
 	 * If an exception is encountered, processing resumes here.
 	 *
@@ -562,6 +582,10 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		bool		archive_cleanup_settings_changed = false;
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -577,6 +601,36 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		/*
+		 * If the archive-cleanup settings have changed, call the currently
+		 * loaded shutdown callback and clear all the restore callbacks.
+		 * There's presently no good way to unload a library besides
+		 * restarting the process, and there should be little harm in leaving
+		 * it around, so we just leave it loaded.
+		 */
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			archive_cleanup_settings_changed = true;
+			call_restore_module_shutdown_cb(0, (Datum) 0);
+			RestoreCallbacks = NULL;
+		}
+
+		/*
+		 * As in CheckpointerMain(), we only emit a WARNING if we detect that
+		 * both restore_library and archive_cleanup_command are set.  We do
+		 * this even if the archive-cleanup settings haven't changed to remind
+		 * the user about the misconfiguration.
+		 */
+		if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											 archiveCleanupCommand, "archive_cleanup_command",
+											 WARNING) &&
+			archive_cleanup_settings_changed)
+			LoadRestoreCallbacks();
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index cc665ce259..f8d3d6b3d2 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -26,6 +26,7 @@
 #include "miscadmin.h"
 #include "postmaster/auxprocess.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "storage/ipc.h"
 #include "storage/pmsignal.h"
 #include "storage/procsignal.h"
@@ -119,13 +120,17 @@ StartupProcShutdownHandler(SIGNAL_ARGS)
  * Re-read the config file.
  *
  * If one of the critical walreceiver options has changed, flag xlog.c
- * to restart it.
+ * to restart it.  Also, check for invalid combinations of the command/library
+ * parameters and reload the restore callbacks if necessary.
  */
 static void
 StartupRereadConfig(void)
 {
 	char	   *conninfo = pstrdup(PrimaryConnInfo);
 	char	   *slotname = pstrdup(PrimarySlotName);
+	char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+	char	   *prevRestoreCommand = pstrdup(recoveryRestoreCommand);
+	char	   *prevRecoveryEndCommand = pstrdup(recoveryEndCommand);
 	bool		tempSlot = wal_receiver_create_temp_slot;
 	bool		conninfoChanged;
 	bool		slotnameChanged;
@@ -147,6 +152,30 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	/*
+	 * If the restore settings have changed, call the currently loaded
+	 * shutdown callback and load the new callbacks.  There's presently no
+	 * good way to unload a library besides restarting the process, and there
+	 * should be little harm in leaving it around, so we just leave it loaded.
+	 */
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_restore_module_shutdown_cb(0, (Datum) 0);
+		LoadRestoreCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
@@ -261,6 +290,19 @@ StartupProcessMain(char *startup_data, size_t startup_data_len)
 	 */
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	LoadRestoreCallbacks();
+
 	/*
 	 * Do what we came for.
 	 */
diff --git a/src/backend/restore/shell_restore.c b/src/backend/restore/shell_restore.c
index 42291625c1..be25f04044 100644
--- a/src/backend/restore/shell_restore.c
+++ b/src/backend/restore/shell_restore.c
@@ -3,7 +3,8 @@
  * shell_restore.c
  *
  * These restore functions use a user-specified shell command (e.g., the
- * restore_command GUC).
+ * restore_command GUC).  It is used as the default, but other modules may
+ * define their own restore logic.
  *
  * Copyright (c) 2024, PostgreSQL Global Development Group
  *
@@ -22,25 +23,76 @@
 #include "common/archive.h"
 #include "common/percentrepl.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static void shell_restore_startup(RestoreModuleState *state);
+static bool shell_restore_file_configured(RestoreModuleState *state);
 static bool shell_restore_file(const char *file, const char *path,
 							   const char *lastRestartPointFileName);
+static bool shell_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool shell_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool shell_archive_cleanup_configured(RestoreModuleState *state);
+static void shell_archive_cleanup(RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+static bool shell_recovery_end_configured(RestoreModuleState *state);
+static void shell_recovery_end(RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
 static void ExecuteRecoveryCommand(const char *command,
 								   const char *commandName,
 								   bool failOnSignal,
 								   uint32 wait_event_info,
 								   const char *lastRestartPointFileName);
 
+static const RestoreModuleCallbacks shell_restore_callbacks = {
+	.startup_cb = shell_restore_startup,
+	.restore_wal_segment_configured_cb = shell_restore_file_configured,
+	.restore_wal_segment_cb = shell_restore_wal_segment,
+	.restore_timeline_history_configured_cb = shell_restore_file_configured,
+	.restore_timeline_history_cb = shell_restore_timeline_history,
+	.timeline_history_exists_configured_cb = shell_restore_file_configured,
+	.timeline_history_exists_cb = shell_restore_timeline_history,
+	.archive_cleanup_configured_cb = shell_archive_cleanup_configured,
+	.archive_cleanup_cb = shell_archive_cleanup,
+	.recovery_end_configured_cb = shell_recovery_end_configured,
+	.recovery_end_cb = shell_recovery_end,
+	.shutdown_cb = NULL
+};
+
+/*
+ * shell_restore_init
+ *
+ * Returns the callbacks for restoring via shell.
+ */
+const RestoreModuleCallbacks *
+shell_restore_init(void)
+{
+	return &shell_restore_callbacks;
+}
+
+/*
+ * Indicates that our timeline_history_exists_cb only attempts to retrieve the
+ * file, so the caller should verify the file exists afterwards.
+ */
+static void
+shell_restore_startup(RestoreModuleState *state)
+{
+	state->timeline_history_exists_cb_copies = true;
+}
+
 /*
  * Attempt to execute a shell-based restore command to retrieve a WAL segment.
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_wal_segment(const char *file, const char *path,
+static bool
+shell_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
 						  const char *lastRestartPointFileName)
 {
 	return shell_restore_file(file, path, lastRestartPointFileName);
@@ -52,8 +104,9 @@ shell_restore_wal_segment(const char *file, const char *path,
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_timeline_history(const char *file, const char *path)
+static bool
+shell_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
 {
 	char		lastRestartPointFname[MAXPGPATH];
 
@@ -64,8 +117,8 @@ shell_restore_timeline_history(const char *file, const char *path)
 /*
  * Check whether restore_command is supplied.
  */
-bool
-shell_restore_file_configured(void)
+static bool
+shell_restore_file_configured(RestoreModuleState *state)
 {
 	return recoveryRestoreCommand[0] != '\0';
 }
@@ -152,8 +205,8 @@ shell_restore_file(const char *file, const char *path,
 /*
  * Check whether archive_cleanup_command is supplied.
  */
-bool
-shell_archive_cleanup_configured(void)
+static bool
+shell_archive_cleanup_configured(RestoreModuleState *state)
 {
 	return archiveCleanupCommand[0] != '\0';
 }
@@ -161,8 +214,9 @@ shell_archive_cleanup_configured(void)
 /*
  * Attempt to execute a shell-based archive cleanup command.
  */
-void
-shell_archive_cleanup(const char *lastRestartPointFileName)
+static void
+shell_archive_cleanup(RestoreModuleState *state,
+					  const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(archiveCleanupCommand, "archive_cleanup_command",
 						   false, WAIT_EVENT_ARCHIVE_CLEANUP_COMMAND,
@@ -172,8 +226,8 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 /*
  * Check whether recovery_end_command is supplied.
  */
-bool
-shell_recovery_end_configured(void)
+static bool
+shell_recovery_end_configured(RestoreModuleState *state)
 {
 	return recoveryEndCommand[0] != '\0';
 }
@@ -181,8 +235,9 @@ shell_recovery_end_configured(void)
 /*
  * Attempt to execute a shell-based end-of-recovery command.
  */
-void
-shell_recovery_end(const char *lastRestartPointFileName)
+static void
+shell_recovery_end(RestoreModuleState *state,
+				   const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(recoveryEndCommand, "recovery_end_command", true,
 						   WAIT_EVENT_RECOVERY_END_COMMAND,
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1e71e7db4a..f84724a0ed 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -70,6 +70,7 @@
 #include "replication/slot.h"
 #include "replication/slotsync.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -3946,6 +3947,16 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"restore_library", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
+			gettext_noop("Sets the library that will be called for restore actions."),
+			NULL
+		},
+		&restoreLibrary,
+		"",
+		NULL, NULL, NULL
+	},
+
 	{
 		{"archive_cleanup_command", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
 			gettext_noop("Sets the shell command that will be executed at every restart point."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 2244ee52f7..5265857519 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -284,6 +284,7 @@
 				# placeholders: %p = path of file to restore
 				#               %f = file name only
 				# e.g. 'cp /mnt/server/archivedir/%f %p'
+#restore_library = ''		# library to use for restore actions
 #archive_cleanup_command = ''	# command to execute at every restartpoint
 #recovery_end_command = ''	# command to execute at completion of recovery
 
diff --git a/src/include/restore/restore_module.h b/src/include/restore/restore_module.h
new file mode 100644
index 0000000000..cc148e855a
--- /dev/null
+++ b/src/include/restore/restore_module.h
@@ -0,0 +1,143 @@
+/*-------------------------------------------------------------------------
+ *
+ * restore_module.h
+ *		Exports for restore modules.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * src/include/restore/restore_module.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef _RESTORE_MODULE_H
+#define _RESTORE_MODULE_H
+
+/*
+ * The value of the restore_library GUC.
+ */
+extern PGDLLIMPORT char *restoreLibrary;
+
+typedef struct RestoreModuleState
+{
+	/*
+	 * Private data pointer for use by a restore module.  This can be used to
+	 * store state for the module that will be passed to each of its
+	 * callbacks.
+	 */
+	void	   *private_data;
+
+	/*
+	 * Indicates whether the callback that checks for timeline existence
+	 * merely copies the file.  If set to true, the server will verify that
+	 * the timeline history file exists after the callback returns.
+	 */
+	bool		timeline_history_exists_cb_copies;
+} RestoreModuleState;
+
+/*
+ * Restore module callbacks
+ *
+ * These callback functions should be defined by restore libraries and returned
+ * via _PG_restore_module_init().  For more information about the purpose of
+ * each callback, refer to the restore modules documentation.
+ */
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state,
+									 const char *file, const char *path,
+									 const char *lastRestartPointFileName);
+typedef bool (*RestoreTimelineHistoryConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state,
+										  const char *file, const char *path);
+typedef bool (*TimelineHistoryExistsConfiguredCB) (RestoreModuleState *state);
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state,
+										 const char *file, const char *path);
+typedef bool (*ArchiveCleanupConfiguredCB) (RestoreModuleState *state);
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+typedef bool (*RecoveryEndConfiguredCB) (RestoreModuleState *state);
+typedef void (*RecoveryEndCB) (RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+
+typedef struct RestoreModuleCallbacks
+{
+	RestoreStartupCB startup_cb;
+	RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+	RestoreWalSegmentCB restore_wal_segment_cb;
+	RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+	RestoreTimelineHistoryCB restore_timeline_history_cb;
+	TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+	TimelineHistoryExistsCB timeline_history_exists_cb;
+	ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+	ArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndConfiguredCB recovery_end_configured_cb;
+	RecoveryEndCB recovery_end_cb;
+	RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+
+/*
+ * Type of the shared library symbol _PG_restore_module_init that is looked up
+ * when loading a restore library.
+ */
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+
+extern PGDLLEXPORT const RestoreModuleCallbacks *_PG_restore_module_init(void);
+
+extern void LoadRestoreCallbacks(void);
+extern void call_restore_module_shutdown_cb(int code, Datum arg);
+
+extern const RestoreModuleCallbacks *RestoreCallbacks;
+extern RestoreModuleState *restore_module_state;
+
+/*
+ * The following *_configured() functions are convenient wrappers around the
+ * *_configured_cb() callback functions with additional defensive NULL checks.
+ */
+
+static inline bool
+restore_wal_segment_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->restore_wal_segment_configured_cb != NULL &&
+		RestoreCallbacks->restore_wal_segment_cb != NULL &&
+		RestoreCallbacks->restore_wal_segment_configured_cb(restore_module_state);
+}
+
+static inline bool
+restore_timeline_history_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->restore_timeline_history_configured_cb != NULL &&
+		RestoreCallbacks->restore_timeline_history_cb != NULL &&
+		RestoreCallbacks->restore_timeline_history_configured_cb(restore_module_state);
+}
+
+static inline bool
+timeline_history_exists_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->timeline_history_exists_configured_cb != NULL &&
+		RestoreCallbacks->timeline_history_exists_cb != NULL &&
+		RestoreCallbacks->timeline_history_exists_configured_cb(restore_module_state);
+}
+
+static inline bool
+archive_cleanup_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->archive_cleanup_configured_cb != NULL &&
+		RestoreCallbacks->archive_cleanup_cb != NULL &&
+		RestoreCallbacks->archive_cleanup_configured_cb(restore_module_state);
+}
+
+static inline bool
+recovery_end_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->recovery_end_configured_cb != NULL &&
+		RestoreCallbacks->recovery_end_cb != NULL &&
+		RestoreCallbacks->recovery_end_configured_cb(restore_module_state);
+}
+
+#endif							/* _RESTORE_MODULE_H */
diff --git a/src/include/restore/shell_restore.h b/src/include/restore/shell_restore.h
index 28b5b7bc92..6352623866 100644
--- a/src/include/restore/shell_restore.h
+++ b/src/include/restore/shell_restore.h
@@ -12,15 +12,13 @@
 #ifndef _SHELL_RESTORE_H
 #define _SHELL_RESTORE_H
 
-extern bool shell_restore_file_configured(void);
-extern bool shell_restore_wal_segment(const char *file, const char *path,
-									  const char *lastRestartPointFileName);
-extern bool shell_restore_timeline_history(const char *file, const char *path);
+#include "restore/restore_module.h"
 
-extern bool shell_archive_cleanup_configured(void);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-
-extern bool shell_recovery_end_configured(void);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Since the logic for restoring via shell commands is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern const RestoreModuleCallbacks *shell_restore_init(void);
 
 #endif							/* _SHELL_RESTORE_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a40c59b2e0..db4cc68a88 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2405,6 +2405,8 @@ ResourceOwnerDesc
 ResourceReleaseCallback
 ResourceReleaseCallbackItem
 ResourceReleasePhase
+RestoreModuleCallbacks
+RestoreModuleState
 RestoreOptions
 RestorePass
 RestrictInfo
-- 
2.25.1


--3V7upXqbjpZ4EhLz--





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

* [PATCH v15 7/7] introduce restore_library
@ 2023-02-16 06:06  Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 28+ messages in thread

From: Nathan Bossart @ 2023-02-16 06:06 UTC (permalink / raw)

---
 contrib/basic_archive/Makefile                |   1 +
 contrib/basic_archive/basic_archive.c         | 153 +++++++-
 contrib/basic_archive/meson.build             |   5 +
 contrib/basic_archive/t/001_restore.pl        |  44 +++
 doc/src/sgml/archive-and-restore-modules.sgml | 356 +++++++++++++++++-
 doc/src/sgml/backup.sgml                      |  40 +-
 doc/src/sgml/basic-archive.sgml               |  31 +-
 doc/src/sgml/config.sgml                      |  53 ++-
 doc/src/sgml/high-availability.sgml           |  18 +-
 src/backend/access/transam/xlog.c             |  28 +-
 src/backend/access/transam/xlogarchive.c      |  96 ++++-
 src/backend/access/transam/xlogrecovery.c     |  14 +-
 src/backend/postmaster/checkpointer.c         |  53 +++
 src/backend/postmaster/startup.c              |  44 ++-
 src/backend/restore/shell_restore.c           |  85 ++++-
 src/backend/utils/misc/guc_tables.c           |  11 +
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/restore/restore_module.h          | 142 +++++++
 src/include/restore/shell_restore.h           |  16 +-
 19 files changed, 1103 insertions(+), 88 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl
 create mode 100644 src/include/restore/restore_module.h

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 55d299d650..d12d45e0d2 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -5,6 +5,7 @@ PGFILEDESC = "basic_archive - basic archive module"
 
 REGRESS = basic_archive
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.conf
+TAP_TESTS = 1
 # Disabled because these tests require "shared_preload_libraries=basic_archive",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index cd852888ce..b04d83da4c 100644
--- a/contrib/basic_archive/basic_archive.c
+++ b/contrib/basic_archive/basic_archive.c
@@ -17,6 +17,11 @@
  * a file is successfully archived and then the system crashes before
  * a durable record of the success has been made.
  *
+ * This file also demonstrates a basic restore library implementation that
+ * is roughly equivalent to the following shell command:
+ *
+ *		cp /path/to/archivedir/%f %p
+ *
  * Copyright (c) 2022-2023, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
@@ -33,6 +38,7 @@
 #include "archive/archive_module.h"
 #include "common/int.h"
 #include "miscadmin.h"
+#include "restore/restore_module.h"
 #include "storage/copydir.h"
 #include "storage/fd.h"
 #include "utils/guc.h"
@@ -54,6 +60,16 @@ static void basic_archive_file_internal(const char *file, const char *path);
 static bool check_archive_directory(char **newval, void **extra, GucSource source);
 static bool compare_files(const char *file1, const char *file2);
 static void basic_archive_shutdown(ArchiveModuleState *state);
+static bool basic_restore_configured(RestoreModuleState *state);
+static bool basic_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool basic_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool basic_restore_file(const char *file, const char *path);
+static bool basic_restore_timeline_history_exists(RestoreModuleState *state,
+												  const char *file,
+												  const char *path);
 
 static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.startup_cb = basic_archive_startup,
@@ -62,6 +78,21 @@ static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.shutdown_cb = basic_archive_shutdown
 };
 
+static const RestoreModuleCallbacks basic_restore_callbacks = {
+	.startup_cb = NULL,
+	.restore_wal_segment_configured_cb = basic_restore_configured,
+	.restore_wal_segment_cb = basic_restore_wal_segment,
+	.restore_timeline_history_configured_cb = basic_restore_configured,
+	.restore_timeline_history_cb = basic_restore_timeline_history,
+	.timeline_history_exists_configured_cb = basic_restore_configured,
+	.timeline_history_exists_cb = basic_restore_timeline_history_exists,
+	.archive_cleanup_configured_cb = NULL,
+	.archive_cleanup_cb = NULL,
+	.recovery_end_configured_cb = NULL,
+	.recovery_end_cb = NULL,
+	.shutdown_cb = NULL
+};
+
 /*
  * _PG_init
  *
@@ -71,7 +102,7 @@ void
 _PG_init(void)
 {
 	DefineCustomStringVariable("basic_archive.archive_directory",
-							   gettext_noop("Archive file destination directory."),
+							   gettext_noop("Archive file source/destination directory."),
 							   NULL,
 							   &archive_directory,
 							   "",
@@ -93,6 +124,17 @@ _PG_archive_module_init(void)
 	return &basic_archive_callbacks;
 }
 
+/*
+ * _PG_restore_module_init
+ *
+ * Returns the module's restore callbacks.
+ */
+const RestoreModuleCallbacks *
+_PG_restore_module_init(void)
+{
+	return &basic_restore_callbacks;
+}
+
 /*
  * basic_archive_startup
  *
@@ -426,3 +468,112 @@ basic_archive_shutdown(ArchiveModuleState *state)
 	pfree(data);
 	state->private_data = NULL;
 }
+
+/*
+ * basic_restore_configured
+ *
+ * Checks that archive_directory is not blank.
+ */
+static bool
+basic_restore_configured(RestoreModuleState *state)
+{
+	return archive_directory != NULL && archive_directory[0] != '\0';
+}
+
+/*
+ * basic_restore_wal_segment
+ *
+ * Retrieves one archived WAL segment.
+ */
+static bool
+basic_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
+						  const char *lastRestartPointFileName)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_timeline_history
+ *
+ * Retrieves one timeline history file.
+ */
+static bool
+basic_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file.
+ */
+static bool
+basic_restore_file(const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive",
+					file)));
+
+	/*
+	 * If the file doesn't exist, return false to indicate that there are no
+	 * more files to restore.
+	 */
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	copy_file(archive_path, path);
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive",
+					file)));
+	return true;
+}
+
+/*
+ * basic_restore_timeline_history_exists
+ *
+ * Check whether a timeline history file is present in the archive directory.
+ */
+static bool
+basic_restore_timeline_history_exists(RestoreModuleState *state,
+									  const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("checking existence of \"%s\" via basic_archive",
+					file)));
+
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	ereport(DEBUG1,
+			(errmsg("verified existence of \"%s\" via basic_archive",
+					file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index bc1380e6f6..4e9f5002de 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -31,4 +31,9 @@ tests += {
     # which typical runningcheck users do not have (e.g. buildfarm clients).
     'runningcheck': false,
   },
+  'tap': {
+    'tests': [
+      't/001_restore.pl',
+    ],
+  },
 }
diff --git a/contrib/basic_archive/t/001_restore.pl b/contrib/basic_archive/t/001_restore.pl
new file mode 100644
index 0000000000..f7fa124695
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,44 @@
+
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# start a node
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init(has_archiving => 1, allows_streaming => 1);
+my $archive_dir = $node->archive_dir;
+$archive_dir =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
+$node->append_conf('postgresql.conf', "archive_command = ''");
+$node->append_conf('postgresql.conf', "archive_library = 'basic_archive'");
+$node->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$node->start;
+
+# backup the node
+my $backup = 'backup';
+$node->backup($backup);
+
+# generate some new WAL files
+$node->safe_psql('postgres', "CREATE TABLE test (a INT);");
+$node->safe_psql('postgres', "SELECT pg_switch_wal();");
+$node->safe_psql('postgres', "INSERT INTO test VALUES (1);");
+
+# shut down the node (this should archive all WAL files)
+$node->stop;
+
+# restore from the backup
+my $restore = PostgreSQL::Test::Cluster->new('restore');
+$restore->init_from_backup($node, $backup, has_restoring => 1, standby => 0);
+$restore->append_conf('postgresql.conf', "restore_command = ''");
+$restore->append_conf('postgresql.conf', "restore_library = 'basic_archive'");
+$restore->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$restore->start;
+
+# ensure post-backup WAL is replayed
+my $result = $restore->poll_query_until("postgres", "SELECT count(*) = 1 FROM test;");
+is($result, "1", "check restore content");
+
+done_testing();
diff --git a/doc/src/sgml/archive-and-restore-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml
index 7ed50a3b52..bf2fdb313a 100644
--- a/doc/src/sgml/archive-and-restore-modules.sgml
+++ b/doc/src/sgml/archive-and-restore-modules.sgml
@@ -8,15 +8,17 @@
 
  <para>
   PostgreSQL provides infrastructure to create custom modules for continuous
-  archiving (see <xref linkend="continuous-archiving"/>).  While archiving via
-  a shell command (i.e., <xref linkend="guc-archive-command"/>) is much
-  simpler, a custom archive module will often be considerably more robust and
-  performant.
+  archiving and recovery (see <xref linkend="continuous-archiving"/>).  While a
+  shell command (i.e., <xref linkend="guc-archive-command"/>,
+  <xref linkend="guc-restore-command"/>) is much simpler, a custom module will
+  often be considerably more robust and performant.
  </para>
 
  <para>
   The <filename>contrib/basic_archive</filename> module contains a working
-  example, which demonstrates some useful techniques.
+  example, which demonstrates some useful techniques.  As demonstrated in
+  <filename>basic_archive</filename> a single module may serve as both an
+  archive module and a restore module.
  </para>
 
  <sect1 id="archive-modules">
@@ -166,4 +168,348 @@ typedef void (*ArchiveShutdownCB) (ArchiveModuleState *state);
    </sect3>
   </sect2>
  </sect1>
+
+ <sect1 id="restore-modules">
+  <title>Restore Modules</title>
+  <indexterm zone="restore-modules">
+   <primary>Restore Modules</primary>
+  </indexterm>
+
+  <para>
+   When a custom <xref linkend="guc-restore-library"/> is configured,
+   PostgreSQL will use the module for restore actions.  It is
+   ultimately up to the module to decide how to accomplish each task,
+   but some recommendations are listed at
+   <xref linkend="backup-pitr-recovery"/>.
+  </para>
+
+  <para>
+   Restore modules must at least consist of an initialization function
+   (see <xref linkend="restore-module-init"/>) and the required callbacks (see
+   <xref linkend="restore-module-callbacks"/>).  However, restore modules are
+   also permitted to do much more (e.g., declare GUCs and register background
+   workers).
+  </para>
+
+  <sect2 id="restore-module-init">
+   <title>Initialization Functions</title>
+   <indexterm zone="restore-module-init">
+    <primary>_PG_restore_module_init</primary>
+   </indexterm>
+
+   <para>
+    An restore library is loaded by dynamically loading a shared library with
+    the <xref linkend="guc-restore-library"/>'s name as the library base name.
+    The normal library search path is used to locate the library.  To provide
+    the required restore module callbacks and to indicate that the library is
+    actually a restore module, it needs to provide a function named
+    <function>_PG_restore_module_init</function>.  The result of the function
+    must be a pointer to a struct of type
+    <structname>RestoreModuleCallbacks</structname>, which contains everything
+    that the core code needs to know how to make use of the restore module.
+    The return value needs to be of server lifetime, which is typically
+    achieved by defining it as a <literal>static const</literal> variable in
+    global scope.
+
+<programlisting>
+typedef struct RestoreModuleCallbacks
+{
+    RestoreStartupCB startup_cb;
+    RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+    RestoreWalSegmentCB restore_wal_segment_cb;
+    RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+    RestoreTimelineHistoryCB restore_timeline_history_cb;
+    TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+    TimelineHistoryExistsCB timeline_history_exists_cb;
+    ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+    ArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndConfiguredCB recovery_end_configured_cb;
+    RecoveryEndCB recovery_end_cb;
+    RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+</programlisting>
+
+    The <function>restore_wal_segment_configured_cb</function>,
+    <function>restore_wal_segment_cb</function>,
+    <function>restore_timeline_history_configured_cb</function>,
+    <function>restore_timeline_history_cb</function>,
+    <function>timeline_history_exists_configured_cb</function>, and
+    <function>timeline_history_exists_cb</function> callbacks are required for
+    archive recovery but optional for streaming replication.  The others are
+    always optional.
+   </para>
+  </sect2>
+
+  <sect2 id="restore-module-callbacks">
+   <title>Restore Module Callbacks</title>
+
+   <para>
+    The restore callbacks define the actual behavior of the module.  The server
+    will call them as required to execute restore actions.
+   </para>
+
+   <sect3 id="restore-module-startup">
+    <title>Startup Callback</title>
+
+    <para>
+     The <function>startup_cb</function> callback is called shortly after the
+     module is loaded.  This callback can be used to perform any additional
+     initialization required.  If the restore module has state, it can use
+     <structfield>state->private_data</structfield> to store it.
+
+<programlisting>
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+</programlisting>
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment-configured">
+    <title>Restore WAL Segment Configured Callback</title>
+    <para>
+     The <function>restore_wal_segment_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to retrieve WAL files (e.g., its configuration parameters
+     are set to valid values).  If no
+     <function>restore_wal_segment_configured_cb</function> is defined, the
+     server always assumes the module is not configured to retrieve WAL files.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve WAL files
+     by calling the <function>restore_wal_segment_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_wal_segment_cb</function> callback to retrieve WAL
+     files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment">
+    <title>Restore WAL Segment Callback</title>
+    <para>
+     The <function>restore_wal_segment_cb</function> callback is called to
+     retrieve a single archived segment of the WAL file series for archive
+     recovery or streaming replication.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state, const char *file, const char *path, const char *lastRestartPointFileName);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point.  That is the earliest
+     file that must be kept to allow a restore to be restartable, so this
+     information can be used to truncate the archive to just the minimum
+     required to support restarting from the current restore.
+     <replaceable>lastRestartPointFileName</replaceable> is typically only used
+     by warm-standby configurations (see <xref linkend="warm-standby"/>).  Note
+     that if multiple standby servers are restoring from the same archive
+     directory, you will need to ensure that you do not delete WAL files until
+     they are no longer needed by any of the servers.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history-configured">
+    <title>Restore Timeline History Configured Callback</title>
+    <para>
+     The <function>restore_timeline_history_configured_cb</function> callback
+     is called to determine whether the module is fully configured and ready to
+     accept requests to retrieve timeline history files (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>restore_timeline_history_configured_cb</function> is defined,
+     the server always assumes the module is not configured to retrieve
+     timeline history files.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve timeline
+     history files by calling the
+     <function>restore_timeline_history_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_timeline_history_cb</function> callback to retrieve
+     timeline history files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history">
+    <title>Restore Timeline History Callback</title>
+    <para>
+     The <function>restore_timeline_history_cb</function> callback is called to
+     retrieve a single archived timeline history file for archive recovery or
+     streaming replication.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists-configured">
+    <title>Timeline History Exists Configured Callback</title>
+    <para>
+     The <function>timeline_history_exists_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to determine whether a timeline history file exists in the
+     archives (e.g., its configuration parameters are set to valid values).  If
+     no <function>timeline_history_exists_configured_cb</function> is defined,
+     the server always assumes the module is not configured to check whether
+     certain timeline history files exist.
+
+<programlisting>
+typedef bool (*TimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will check whether
+     certain timeline history files are present in the archives by calling the
+     <function>timeline_history_exists_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>timeline_history_exists_cb</function> callback to check if
+     timeline history files exist in the archives.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists">
+    <title>Timeline History Exists Callback</title>
+    <para>
+     The <function>timeline_history_exists_cb</function> callback is called to
+     check whether a timeline history file is present in the archives.
+
+<programlisting>
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file is
+     present in the archives.  If the file is not available in the archives,
+     the callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the timeline history file.
+    </para>
+
+    <para>
+     Some restore modules might not have a dedicated way to determine whether a
+     timeline history file exists.  For example,
+     <varname>restore_command</varname> only tells the server how to retrieve
+     files.  In this case, the <function>timeline_history_exists_cb</function>
+     callback should copy the file to <replaceable>path</replaceable>, which
+     contains the destination's relative path (including the file name), and
+     the restore module should set
+     <structfield>state->timeline_history_exists_cb_copies</structfield> to
+     <literal>true</literal>.  It is recommended to set this flag in the
+     module's <function>startup_cb</function> callback.  This flag tells the
+     server that it should verify that the file was successfully retrieved
+     after invoking this callback.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup-configured">
+    <title>Archive Cleanup Configured Callback</title>
+    <para>
+     The <function>archive_cleanup_configured_cb</function> callback is called
+     to determine whether the module is fully configured and ready to accept
+     requests to remove old WAL files from the archives (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>archive_cleanup_configured_cb</function> is defined, the server
+     always assumes the module is not configured to remove old WAL files.
+
+<programlisting>
+typedef bool (*ArchiveCleanupConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will remove old WAL
+     files by calling the <function>archive_cleanup_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>archive_cleanup_cb</function> callback to remove old WAL files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup">
+    <title>Archive Cleanup Callback</title>
+    <para>
+     The <function>archive_cleanup_cb</function> callback is called at every
+     restart point by the checkpointer process and is intended to provide a
+     mechanism for cleaning up old archived WAL files that are no longer
+     needed by the standby server.
+
+<programlisting>
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the
+     name of the file that includes the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end-configured">
+    <title>Recovery End Configured Callback</title>
+    <para>
+     The <function>recovery_end_configured_cb</function> callback is called to
+     determine whether the module is fully configured and ready to execute its
+     <function>recovery_end_cb</function> callback once at the end of recovery
+     (e.g., its configuration parameters are set to valid values).  If no
+     <function>recovery_end_configured_cb</function> is defined, the server
+     always assumes the module is not configured to execute its
+     <function>recovery_end_cb</function> callback.
+
+<programlisting>
+typedef bool (*RecoveryEndConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will execute the
+     <function>recovery_end_cb</function> callback once at the end of recovery.
+     If <literal>false</literal> is returned, the server will not use the
+     <function>recovery_end_cb</function> callback at the end of recovery.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end">
+    <title>Recovery End Callback</title>
+    <para>
+     The <function>recovery_end_cb</function> callback is called once at the
+     end of recovery and is intended to provide a mechanism for cleanup
+     following the end of replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-shutdown">
+    <title>Shutdown Callback</title>
+    <para>
+     The <function>shutdown_cb</function> callback is called when a process
+     that has loaded the restore module exits (e.g., after an error) or the
+     value of <xref linkend="guc-restore-library"/> changes.  If no
+     <function>shutdown_cb</function> is defined, no special action is taken in
+     these situations.  If the restore module has state, this callback should
+     free it to avoid leaks.
+
+<programlisting>
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+ </programlisting>
+    </para>
+   </sect3>
+  </sect2>
+ </sect1>
 </chapter>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index 8cb24d6ae5..4b20651234 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1180,9 +1180,26 @@ SELECT * FROM pg_backup_stop(wait_for_archive => true);
    <para>
     The key part of all this is to set up a recovery configuration that
     describes how you want to recover and how far the recovery should
-    run.  The one thing that you absolutely must specify is the <varname>restore_command</varname>,
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>,
     which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    WAL file segments.
+   </para>
+
+   <para>
+    Like the <varname>archive_library</varname> parameter,
+    <varname>restore_library</varname> is a shared library.  Since such
+    libraries are written in <literal>C</literal>, creating your own may
+    require considerably more effort than writing a shell command.  However,
+    restore modules can be more performance than restoring via shell, and they
+    will have access to many useful server resources.  For more information
+    about creating a <varname>restore_library</varname>, see
+    <xref linkend="restore-modules"/>.
+   </para>
+
+   <para>
+    Like the <varname>archive_command</varname>,
+    <varname>restore_command</varname> is
     a shell command string.  It can contain <literal>%f</literal>, which is
     replaced by the name of the desired WAL file, and <literal>%p</literal>,
     which is replaced by the path name to copy the WAL file to.
@@ -1201,14 +1218,20 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
    </para>
 
    <para>
-    It is important that the command return nonzero exit status on failure.
-    The command <emphasis>will</emphasis> be called requesting files that are not
-    present in the archive; it must return nonzero when so asked.  This is not
-    an error condition.  An exception is that if the command was terminated by
+    It is important that the <varname>restore_command</varname> return nonzero
+    exit status on failure, or, if you are using a
+    <varname>restore_library</varname>, that the restore callbacks return
+    <literal>false</literal> on failure.  The command or library
+    <emphasis>will</emphasis> be called requesting files that are not
+    present in the archive; it must fail when so asked.  This is not
+    an error condition.  An exception is that if the
+    <varname>restore_command</varname> was terminated by
     a signal (other than <systemitem>SIGTERM</systemitem>, which is used as
     part of a database server shutdown) or an error by the shell (such as
     command not found), then recovery will abort and the server will not start
-    up.
+    up.  Likewise, if the restore callbacks provided by the
+    <varname>restore_library</varname> emit an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1232,7 +1255,8 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
     close as possible given the available WAL segments).  Therefore, a normal
     recovery will end with a <quote>file not found</quote> message, the exact text
     of the error message depending upon your choice of
-    <varname>restore_command</varname>.  You may also see an error message
+    <varname>restore_command</varname> or <varname>restore_library</varname>.
+    You may also see an error message
     at the start of recovery for a file named something like
     <filename>00000001.history</filename>.  This is also normal and does not
     indicate a problem in simple recovery situations; see
diff --git a/doc/src/sgml/basic-archive.sgml b/doc/src/sgml/basic-archive.sgml
index b4d43ced20..f3a5a502bd 100644
--- a/doc/src/sgml/basic-archive.sgml
+++ b/doc/src/sgml/basic-archive.sgml
@@ -8,17 +8,20 @@
  </indexterm>
 
  <para>
-  <filename>basic_archive</filename> is an example of an archive module.  This
-  module copies completed WAL segment files to the specified directory.  This
-  may not be especially useful, but it can serve as a starting point for
-  developing your own archive module.  For more information about archive
-  modules, see <xref linkend="archive-modules"/>.
+  <filename>basic_archive</filename> is an example of an archive and restore
+  module.  This module copies completed WAL segment files to or from the
+  specified directory.  This may not be especially useful, but it can serve as
+  a starting point for developing your own archive and restore modules.  For
+  more information about archive and restore modules, see\
+  <xref linkend="archive-and-restore-modules"/>.
  </para>
 
  <para>
-  In order to function, this module must be loaded via
+  For use as an archive module, this module must be loaded via
   <xref linkend="guc-archive-library"/>, and <xref linkend="guc-archive-mode"/>
-  must be enabled.
+  must be enabled.  For use as a restore module, this module must be loaded
+  via <xref linkend="guc-restore-library"/>, and recovery must be enabled (see
+  <xref linkend="runtime-config-wal-archive-recovery"/>).
  </para>
 
  <sect2 id="basic-archive-configuration-parameters">
@@ -34,11 +37,12 @@
     </term>
     <listitem>
      <para>
-      The directory where the server should copy WAL segment files.  This
-      directory must already exist.  The default is an empty string, which
-      effectively halts WAL archiving, but if <xref linkend="guc-archive-mode"/>
-      is enabled, the server will accumulate WAL segment files in the
-      expectation that a value will soon be provided.
+      The directory where the server should copy WAL segment files to or from.
+      This directory must already exist.  The default is an empty string,
+      which, when used for archiving, effectively halts WAL archival, but if
+      <xref linkend="guc-archive-mode"/> is enabled, the server will accumulate
+      WAL segment files in the expectation that a value will soon be provided.
+      When an empty string is used for recovery, restore will fail.
      </para>
     </listitem>
    </varlistentry>
@@ -46,7 +50,7 @@
 
   <para>
    These parameters must be set in <filename>postgresql.conf</filename>.
-   Typical usage might be:
+   Typical usage as an archive module might be:
   </para>
 
 <programlisting>
@@ -61,6 +65,7 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
+   When <filename>basic_archive</filename> is used as an archive module,
    Server crashes may leave temporary files with the prefix
    <filename>archtemp</filename> in the archive directory.  It is recommended to
    delete such files before restarting the server after a crash.  It is safe to
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 091a79d4f3..4b0dd21108 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3873,7 +3873,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
@@ -3901,7 +3902,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,
@@ -3936,7 +3938,42 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
 
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>restore_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-restore-library" xreflabel="restore_library">
+      <term><varname>restore_library</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>restore_library</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        The library to use for restore actions, including retrieving archived
+        files and executing tasks at restartpoints and at recovery end.  Either
+        <xref linkend="guc-restore-command"/> or
+        <varname>restore_library</varname> is required for archive recovery,
+        but optional for streaming replication.  If this parameter is set to an
+        empty string (the default), restoring via shell is enabled, and
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, and
+        <varname>recovery_end_command</varname> are used.  If both
+        <varname>restore_library</varname> and any of
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, or
+        <varname>recovery_end_command</varname> are set, an error will be
+        raised.  Otherwise, the specified shared library is used for restoring.
+        For more information, see <xref linkend="restore-modules"/>.
+       </para>
+
+       <para>
+        This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
        </para>
       </listitem>
      </varlistentry>
@@ -3981,7 +4018,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>
@@ -4010,7 +4050,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
        </para>
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>recovery_end_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index 9d0deaeeb8..094140cecc 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -627,7 +627,8 @@ protocol to make nodes agree on a serializable transactional order.
    <para>
     In standby mode, the server continuously applies WAL received from the
     primary server. The standby server can read WAL from a WAL archive
-    (see <xref linkend="guc-restore-command"/>) or directly from the primary
+    (see <xref linkend="guc-restore-command"/> and
+    <xref linkend="guc-restore-library"/>) or directly from the primary
     over a TCP connection (streaming replication). The standby server will
     also attempt to restore any WAL found in the standby cluster's
     <filename>pg_wal</filename> directory. That typically happens after a server
@@ -638,8 +639,10 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     At startup, the standby begins by restoring all WAL available in the
-    archive location, calling <varname>restore_command</varname>. Once it
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s callbacks.  Once it
     reaches the end of WAL available there and <varname>restore_command</varname>
+    or <varname>restore_library</varname>
     fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
     If that fails, and streaming replication has been configured, the
     standby tries to connect to the primary server and start streaming WAL
@@ -698,7 +701,8 @@ protocol to make nodes agree on a serializable transactional order.
     server (see <xref linkend="backup-pitr-recovery"/>). Create a file
     <link linkend="file-standby-signal"><filename>standby.signal</filename></link><indexterm><primary>standby.signal</primary></indexterm>
     in the standby's cluster data
-    directory. Set <xref linkend="guc-restore-command"/> to a simple command to copy files from
+    directory. Set <xref linkend="guc-restore-command"/> or
+    <xref linkend="guc-restore-library"/> to copy files from
     the WAL archive. If you plan to have multiple standby servers for high
     availability purposes, make sure that <varname>recovery_target_timeline</varname> is set to
     <literal>latest</literal> (the default), to make the standby server follow the timeline change
@@ -707,7 +711,8 @@ protocol to make nodes agree on a serializable transactional order.
 
    <note>
      <para>
-     <xref linkend="guc-restore-command"/> should return immediately
+     <xref linkend="guc-restore-command"/> and
+     <xref linkend="guc-restore-library"/> should return immediately
      if the file does not exist; the server will retry the command again if
      necessary.
     </para>
@@ -731,8 +736,9 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     If you're using a WAL archive, its size can be minimized using the <xref
-    linkend="guc-archive-cleanup-command"/> parameter to remove files that are no
-    longer required by the standby server.
+    linkend="guc-archive-cleanup-command"/> parameter or the
+    <xref linkend="guc-restore-library"/>'s archive cleanup callbacks to remove
+    files that are no longer required by the standby server.
     The <application>pg_archivecleanup</application> utility is designed specifically to
     be used with <varname>archive_cleanup_command</varname> in typical single-standby
     configurations, see <xref linkend="pgarchivecleanup"/>.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index eaba35b59e..79d33e565a 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -84,7 +84,7 @@
 #include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
-#include "restore/shell_restore.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
@@ -4904,18 +4904,19 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during log
-	 * shipping replication.  All files earlier than this point can be deleted
-	 * from the archive, though there is no requirement to do so.
+	 * The callback is provided the archive file cutoff point for use during
+	 * log shipping replication.  All files earlier than this point can be
+	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_recovery_end_configured())
+	if (recovery_end_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_recovery_end(lastRestartPointFname);
+		RestoreCallbacks->recovery_end_cb(restore_module_state,
+										  lastRestartPointFname);
 	}
 
 	/*
@@ -7334,18 +7335,19 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Finally, execute archive-cleanup callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during log
-	 * shipping replication.  All files earlier than this point can be deleted
-	 * from the archive, though there is no requirement to do so.
+	 * The callback is provided the archive file cutoff point for use during
+	 * log shipping replication.  All files earlier than this point can be
+	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_archive_cleanup_configured())
+	if (archive_cleanup_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_archive_cleanup(lastRestartPointFname);
+		RestoreCallbacks->archive_cleanup_cb(restore_module_state,
+											 lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index a925ac22f6..b1354dcee9 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -23,15 +23,22 @@
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
 #include "common/archive.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/startup.h"
 #include "postmaster/pgarch.h"
 #include "replication/walsender.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/lwlock.h"
+#include "utils/memutils.h"
+
+char *restoreLibrary = "";
+const RestoreModuleCallbacks *RestoreCallbacks = NULL;
+RestoreModuleState *restore_module_state;
 
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
@@ -75,15 +82,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			if (!shell_restore_file_configured())
+			if (!restore_wal_segment_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			if (!shell_restore_file_configured())
+			if (!restore_timeline_history_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			if (!shell_restore_file_configured())
+			if (!timeline_history_exists_configured())
 				goto not_available;
 			break;
 	}
@@ -175,14 +182,17 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			ret = shell_restore_wal_segment(xlogfname, xlogpath,
-											lastRestartPointFname);
+			ret = RestoreCallbacks->restore_wal_segment_cb(restore_module_state,
+														   xlogfname, xlogpath,
+														   lastRestartPointFname);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->restore_timeline_history_cb(restore_module_state,
+																xlogfname, xlogpath);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->timeline_history_exists_cb(restore_module_state,
+															   xlogfname, xlogpath);
 			break;
 	}
 
@@ -190,6 +200,16 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 
 	if (ret)
 	{
+		/*
+		 * Some restore functions might not copy the file (e.g., checking
+		 * whether a timeline history file exists), but they can set a flag to
+		 * tell us if they do.  We only need to verify file existence if this
+		 * flag is enabled.
+		 */
+		if (archive_type == ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS &&
+			!restore_module_state->timeline_history_exists_cb_copies)
+			return true;
+
 		/*
 		 * command apparently succeeded, but let's make sure the file is
 		 * really there now and has the correct size.
@@ -631,3 +651,65 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the restore callbacks into our global RestoreCallbacks.  The
+ * caller is responsible for validating the combination of library/command
+ * parameters that are set (e.g., restore_command and restore_library cannot
+ * both be set).
+ */
+void
+LoadRestoreCallbacks(void)
+{
+	RestoreModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_restore_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RestoreModuleInit)
+				load_external_function(restoreLibrary, "_PG_restore_module_init",
+									   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("restore modules have to define the symbol "
+						"_PG_restore_module_init")));
+
+	RestoreCallbacks = (*init) ();
+
+	/* restore state should be freed before calling this function */
+	Assert(restore_module_state == NULL);
+	restore_module_state = (RestoreModuleState *)
+							MemoryContextAllocZero(TopMemoryContext,
+												   sizeof(RestoreModuleState));
+
+	if (RestoreCallbacks->startup_cb != NULL)
+		RestoreCallbacks->startup_cb(restore_module_state);
+}
+
+/*
+ * Call the shutdown callback of the loaded restore module, if defined.  Also,
+ * free the restore module state if it was allocated.
+ *
+ * Processes that load restore libraries should register this via
+ * before_shmem_exit().
+ */
+void
+call_restore_module_shutdown_cb(int code, Datum arg)
+{
+	if (RestoreCallbacks != NULL &&
+		RestoreCallbacks->shutdown_cb != NULL &&
+		restore_module_state != NULL)
+		RestoreCallbacks->shutdown_cb(restore_module_state);
+
+	if (restore_module_state != NULL)
+	{
+		pfree(restore_module_state);
+		restore_module_state = NULL;
+	}
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index e3b02ed760..9153e7bac0 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -50,6 +50,7 @@
 #include "postmaster/startup.h"
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
+#include "restore/restore_module.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1078,18 +1079,21 @@ validateRecoveryParameters(void)
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			(!restore_wal_segment_configured() ||
+			 !restore_timeline_history_configured() ||
+			 !timeline_history_exists_configured()))
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
+					(errmsg("specified neither primary_conninfo nor restore_command nor a configured restore_library"),
 					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
 	}
 	else
 	{
-		if (recoveryRestoreCommand == NULL ||
-			strcmp(recoveryRestoreCommand, "") == 0)
+		if (!restore_wal_segment_configured() ||
+			!restore_timeline_history_configured() ||
+			!timeline_history_exists_configured())
 			ereport(FATAL,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("must specify restore_command when standby mode is not enabled")));
+					 errmsg("must specify restore_command or a configured restore_library when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index ace9893d95..403f4051c0 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -45,6 +45,7 @@
 #include "postmaster/bgwriter.h"
 #include "postmaster/interrupt.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -233,6 +234,24 @@ CheckpointerMain(void)
 												 ALLOCSET_DEFAULT_SIZES);
 	MemoryContextSwitchTo(checkpointer_context);
 
+	/*
+	 * Load restore_library so that we can use its archive-cleanup callback, if
+	 * one is defined.
+	 *
+	 * We also take this opportunity to set up the before_shmem_exit hook for
+	 * the shutdown callback and to check that only one of restore_library,
+	 * archive_cleanup_command is set.  Note that we emit a WARNING upon
+	 * detecting misconfigured parameters, and we clear the callbacks so that
+	 * the archive-cleanup functionality is skipped.  We judge this
+	 * functionality to not be important enough to require blocking checkpoints
+	 * or shutting down the server when the parameters are misconfigured.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+										 archiveCleanupCommand, "archive_cleanup_command",
+										 WARNING))
+		LoadRestoreCallbacks();
+
 	/*
 	 * If an exception is encountered, processing resumes here.
 	 *
@@ -547,6 +566,10 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		bool		archive_cleanup_settings_changed = false;
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -562,6 +585,36 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		/*
+		 * If the archive-cleanup settings have changed, call the currently
+		 * loaded shutdown callback and clear all the restore callbacks.
+		 * There's presently no good way to unload a library besides restarting
+		 * the process, and there should be little harm in leaving it around,
+		 * so we just leave it loaded.
+		 */
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			archive_cleanup_settings_changed = true;
+			call_restore_module_shutdown_cb(0, (Datum) 0);
+			RestoreCallbacks = NULL;
+		}
+
+		/*
+		 * As in CheckpointerMain(), we only emit a WARNING if we detect that
+		 * both restore_library and archive_cleanup_command are set.  We do
+		 * this even if the archive-cleanup settings haven't changed to remind
+		 * the user about the misconfiguration.
+		 */
+		if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											 archiveCleanupCommand, "archive_cleanup_command",
+											 WARNING) &&
+			archive_cleanup_settings_changed)
+			LoadRestoreCallbacks();
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index adce0ffcef..a096b79182 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -29,6 +29,7 @@
 #include "pgstat.h"
 #include "postmaster/interrupt.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
 #include "storage/pmsignal.h"
@@ -148,13 +149,17 @@ StartupProcShutdownHandler(SIGNAL_ARGS)
  * Re-read the config file.
  *
  * If one of the critical walreceiver options has changed, flag xlog.c
- * to restart it.
+ * to restart it.  Also, check for invalid combinations of the command/library
+ * parameters and reload the restore callbacks if necessary.
  */
 static void
 StartupRereadConfig(void)
 {
 	char	   *conninfo = pstrdup(PrimaryConnInfo);
 	char	   *slotname = pstrdup(PrimarySlotName);
+	char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+	char	   *prevRestoreCommand = pstrdup(recoveryRestoreCommand);
+	char	   *prevRecoveryEndCommand = pstrdup(recoveryEndCommand);
 	bool		tempSlot = wal_receiver_create_temp_slot;
 	bool		conninfoChanged;
 	bool		slotnameChanged;
@@ -176,6 +181,30 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	/*
+	 * If the restore settings have changed, call the currently loaded shutdown
+	 * callback and load the new callbacks.  There's presently no good way to
+	 * unload a library besides restarting the process, and there should be
+	 * little harm in leaving it around, so we just leave it loaded.
+	 */
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_restore_module_shutdown_cb(0, (Datum) 0);
+		LoadRestoreCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
@@ -285,6 +314,19 @@ StartupProcessMain(void)
 	 */
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	LoadRestoreCallbacks();
+
 	/*
 	 * Do what we came for.
 	 */
diff --git a/src/backend/restore/shell_restore.c b/src/backend/restore/shell_restore.c
index fcd5475c88..b67cd4ac48 100644
--- a/src/backend/restore/shell_restore.c
+++ b/src/backend/restore/shell_restore.c
@@ -3,7 +3,8 @@
  * shell_restore.c
  *
  * These restore functions use a user-specified shell command (e.g., the
- * restore_command GUC).
+ * restore_command GUC).  It is used as the default, but other modules may
+ * define their own restore logic.
  *
  * Copyright (c) 2023, PostgreSQL Global Development Group
  *
@@ -22,25 +23,76 @@
 #include "common/archive.h"
 #include "common/percentrepl.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static void shell_restore_startup(RestoreModuleState *state);
+static bool shell_restore_file_configured(RestoreModuleState *state);
 static bool shell_restore_file(const char *file, const char *path,
 							   const char *lastRestartPointFileName);
+static bool shell_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool shell_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool shell_archive_cleanup_configured(RestoreModuleState *state);
+static void shell_archive_cleanup(RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+static bool shell_recovery_end_configured(RestoreModuleState *state);
+static void shell_recovery_end(RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
 static void ExecuteRecoveryCommand(const char *command,
 								   const char *commandName,
 								   bool failOnSignal,
 								   uint32 wait_event_info,
 								   const char *lastRestartPointFileName);
 
+static const RestoreModuleCallbacks shell_restore_callbacks = {
+	.startup_cb = shell_restore_startup,
+	.restore_wal_segment_configured_cb = shell_restore_file_configured,
+	.restore_wal_segment_cb = shell_restore_wal_segment,
+	.restore_timeline_history_configured_cb = shell_restore_file_configured,
+	.restore_timeline_history_cb = shell_restore_timeline_history,
+	.timeline_history_exists_configured_cb = shell_restore_file_configured,
+	.timeline_history_exists_cb = shell_restore_timeline_history,
+	.archive_cleanup_configured_cb = shell_archive_cleanup_configured,
+	.archive_cleanup_cb = shell_archive_cleanup,
+	.recovery_end_configured_cb = shell_recovery_end_configured,
+	.recovery_end_cb = shell_recovery_end,
+	.shutdown_cb = NULL
+};
+
+/*
+ * shell_restore_init
+ *
+ * Returns the callbacks for restoring via shell.
+ */
+const RestoreModuleCallbacks *
+shell_restore_init(void)
+{
+	return &shell_restore_callbacks;
+}
+
+/*
+ * Indicates that our timeline_history_exists_cb only attempts to retrieve the
+ * file, so the caller should verify the file exists afterwards.
+ */
+static void
+shell_restore_startup(RestoreModuleState *state)
+{
+	state->timeline_history_exists_cb_copies = true;
+}
+
 /*
  * Attempt to execute a shell-based restore command to retrieve a WAL segment.
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_wal_segment(const char *file, const char *path,
+static bool
+shell_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
 						  const char *lastRestartPointFileName)
 {
 	return shell_restore_file(file, path, lastRestartPointFileName);
@@ -52,8 +104,9 @@ shell_restore_wal_segment(const char *file, const char *path,
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_timeline_history(const char *file, const char *path)
+static bool
+shell_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
 {
 	char		lastRestartPointFname[MAXPGPATH];
 
@@ -64,8 +117,8 @@ shell_restore_timeline_history(const char *file, const char *path)
 /*
  * Check whether restore_command is supplied.
  */
-bool
-shell_restore_file_configured(void)
+static bool
+shell_restore_file_configured(RestoreModuleState *state)
 {
 	return recoveryRestoreCommand[0] != '\0';
 }
@@ -153,8 +206,8 @@ shell_restore_file(const char *file, const char *path,
 /*
  * Check whether archive_cleanup_command is supplied.
  */
-bool
-shell_archive_cleanup_configured(void)
+static bool
+shell_archive_cleanup_configured(RestoreModuleState *state)
 {
 	return archiveCleanupCommand[0] != '\0';
 }
@@ -162,8 +215,9 @@ shell_archive_cleanup_configured(void)
 /*
  * Attempt to execute a shell-based archive cleanup command.
  */
-void
-shell_archive_cleanup(const char *lastRestartPointFileName)
+static void
+shell_archive_cleanup(RestoreModuleState *state,
+					  const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(archiveCleanupCommand, "archive_cleanup_command",
 						   false, WAIT_EVENT_ARCHIVE_CLEANUP_COMMAND,
@@ -173,8 +227,8 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 /*
  * Check whether recovery_end_command is supplied.
  */
-bool
-shell_recovery_end_configured(void)
+static bool
+shell_recovery_end_configured(RestoreModuleState *state)
 {
 	return recoveryEndCommand[0] != '\0';
 }
@@ -182,8 +236,9 @@ shell_recovery_end_configured(void)
 /*
  * Attempt to execute a shell-based end-of-recovery command.
  */
-void
-shell_recovery_end(const char *lastRestartPointFileName)
+static void
+shell_recovery_end(RestoreModuleState *state,
+				   const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(recoveryEndCommand, "recovery_end_command", true,
 						   WAIT_EVENT_RECOVERY_END_COMMAND,
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index cab3ddbe11..81e4d6d183 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -65,6 +65,7 @@
 #include "replication/logicallauncher.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -3839,6 +3840,16 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"restore_library", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
+			gettext_noop("Sets the library that will be called for restore actions."),
+			NULL
+		},
+		&restoreLibrary,
+		"",
+		NULL, NULL, NULL
+	},
+
 	{
 		{"archive_cleanup_command", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
 			gettext_noop("Sets the shell command that will be executed at every restart point."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index dce5049bc2..5b96fe5ea8 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -275,6 +275,7 @@
 				# placeholders: %p = path of file to restore
 				#               %f = file name only
 				# e.g. 'cp /mnt/server/archivedir/%f %p'
+#restore_library = ''		# library to use for restore actions
 #archive_cleanup_command = ''	# command to execute at every restartpoint
 #recovery_end_command = ''	# command to execute at completion of recovery
 
diff --git a/src/include/restore/restore_module.h b/src/include/restore/restore_module.h
new file mode 100644
index 0000000000..c0aef33ba5
--- /dev/null
+++ b/src/include/restore/restore_module.h
@@ -0,0 +1,142 @@
+/*-------------------------------------------------------------------------
+ *
+ * restore_module.h
+ *		Exports for restore modules.
+ *
+ * Copyright (c) 2023, PostgreSQL Global Development Group
+ *
+ * src/include/restore/restore_module.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef _RESTORE_MODULE_H
+#define _RESTORE_MODULE_H
+
+/*
+ * The value of the restore_library GUC.
+ */
+extern PGDLLIMPORT char *restoreLibrary;
+
+typedef struct RestoreModuleState
+{
+	/*
+	 * Private data pointer for use by a restore module.  This can be used to
+	 * store state for the module that will be passed to each of its callbacks.
+	 */
+	void	   *private_data;
+
+	/*
+	 * Indicates whether the callback that checks for timeline existence merely
+	 * copies the file.  If set to true, the server will verify that the
+	 * timeline history file exists after the callback returns.
+	 */
+	bool		timeline_history_exists_cb_copies;
+} RestoreModuleState;
+
+/*
+ * Restore module callbacks
+ *
+ * These callback functions should be defined by restore libraries and returned
+ * via _PG_restore_module_init().  For more information about the purpose of
+ * each callback, refer to the restore modules documentation.
+ */
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state,
+									 const char *file, const char *path,
+									 const char *lastRestartPointFileName);
+typedef bool (*RestoreTimelineHistoryConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state,
+										  const char *file, const char *path);
+typedef bool (*TimelineHistoryExistsConfiguredCB) (RestoreModuleState *state);
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state,
+										 const char *file, const char *path);
+typedef bool (*ArchiveCleanupConfiguredCB) (RestoreModuleState *state);
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+typedef bool (*RecoveryEndConfiguredCB) (RestoreModuleState *state);
+typedef void (*RecoveryEndCB) (RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+
+typedef struct RestoreModuleCallbacks
+{
+	RestoreStartupCB startup_cb;
+	RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+	RestoreWalSegmentCB restore_wal_segment_cb;
+	RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+	RestoreTimelineHistoryCB restore_timeline_history_cb;
+	TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+	TimelineHistoryExistsCB timeline_history_exists_cb;
+	ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+	ArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndConfiguredCB recovery_end_configured_cb;
+	RecoveryEndCB recovery_end_cb;
+	RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+
+/*
+ * Type of the shared library symbol _PG_restore_module_init that is looked up
+ * when loading a restore library.
+ */
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+
+extern PGDLLEXPORT const RestoreModuleCallbacks *_PG_restore_module_init(void);
+
+extern void LoadRestoreCallbacks(void);
+extern void call_restore_module_shutdown_cb(int code, Datum arg);
+
+extern const RestoreModuleCallbacks *RestoreCallbacks;
+extern RestoreModuleState *restore_module_state;
+
+/*
+ * The following *_configured() functions are convenient wrappers around the
+ * *_configured_cb() callback functions with additional defensive NULL checks.
+ */
+
+static inline bool
+restore_wal_segment_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		   RestoreCallbacks->restore_wal_segment_configured_cb != NULL &&
+		   RestoreCallbacks->restore_wal_segment_cb != NULL &&
+		   RestoreCallbacks->restore_wal_segment_configured_cb(restore_module_state);
+}
+
+static inline bool
+restore_timeline_history_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		   RestoreCallbacks->restore_timeline_history_configured_cb != NULL &&
+		   RestoreCallbacks->restore_timeline_history_cb != NULL &&
+		   RestoreCallbacks->restore_timeline_history_configured_cb(restore_module_state);
+}
+
+static inline bool
+timeline_history_exists_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		   RestoreCallbacks->timeline_history_exists_configured_cb != NULL &&
+		   RestoreCallbacks->timeline_history_exists_cb != NULL &&
+		   RestoreCallbacks->timeline_history_exists_configured_cb(restore_module_state);
+}
+
+static inline bool
+archive_cleanup_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		   RestoreCallbacks->archive_cleanup_configured_cb != NULL &&
+		   RestoreCallbacks->archive_cleanup_cb != NULL &&
+		   RestoreCallbacks->archive_cleanup_configured_cb(restore_module_state);
+}
+
+static inline bool
+recovery_end_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		   RestoreCallbacks->recovery_end_configured_cb != NULL &&
+		   RestoreCallbacks->recovery_end_cb != NULL &&
+		   RestoreCallbacks->recovery_end_configured_cb(restore_module_state);
+}
+
+#endif							/* _RESTORE_MODULE_H */
diff --git a/src/include/restore/shell_restore.h b/src/include/restore/shell_restore.h
index 570588e493..1eeae51d81 100644
--- a/src/include/restore/shell_restore.h
+++ b/src/include/restore/shell_restore.h
@@ -12,15 +12,13 @@
 #ifndef _SHELL_RESTORE_H
 #define _SHELL_RESTORE_H
 
-extern bool shell_restore_file_configured(void);
-extern bool shell_restore_wal_segment(const char *file, const char *path,
-									  const char *lastRestartPointFileName);
-extern bool shell_restore_timeline_history(const char *file, const char *path);
+#include "restore/restore_module.h"
 
-extern bool shell_archive_cleanup_configured(void);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-
-extern bool shell_recovery_end_configured(void);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Since the logic for restoring via shell commands is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern const RestoreModuleCallbacks *shell_restore_init(void);
 
 #endif							/* _SHELL_RESTORE_H */
-- 
2.25.1


--k+w/mQv8wyuph6w0--





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

* [PATCH v12 6/6] introduce restore_library
@ 2023-02-16 06:06  Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 28+ messages in thread

From: Nathan Bossart @ 2023-02-16 06:06 UTC (permalink / raw)

---
 contrib/basic_archive/Makefile                |   1 +
 contrib/basic_archive/basic_archive.c         | 153 +++++++-
 contrib/basic_archive/meson.build             |   5 +
 contrib/basic_archive/t/001_restore.pl        |  44 +++
 doc/src/sgml/archive-and-restore-modules.sgml | 356 +++++++++++++++++-
 doc/src/sgml/backup.sgml                      |  40 +-
 doc/src/sgml/basic-archive.sgml               |  31 +-
 doc/src/sgml/config.sgml                      |  53 ++-
 doc/src/sgml/high-availability.sgml           |  18 +-
 src/backend/access/transam/xlog.c             |  28 +-
 src/backend/access/transam/xlogarchive.c      |  96 ++++-
 src/backend/access/transam/xlogrecovery.c     |  14 +-
 src/backend/postmaster/checkpointer.c         |  53 +++
 src/backend/postmaster/startup.c              |  44 ++-
 src/backend/restore/shell_restore.c           |  85 ++++-
 src/backend/utils/misc/guc_tables.c           |  11 +
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/restore/restore_module.h          | 142 +++++++
 src/include/restore/shell_restore.h           |  16 +-
 19 files changed, 1103 insertions(+), 88 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl
 create mode 100644 src/include/restore/restore_module.h

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 55d299d650..d12d45e0d2 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -5,6 +5,7 @@ PGFILEDESC = "basic_archive - basic archive module"
 
 REGRESS = basic_archive
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.conf
+TAP_TESTS = 1
 # Disabled because these tests require "shared_preload_libraries=basic_archive",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index cd852888ce..b04d83da4c 100644
--- a/contrib/basic_archive/basic_archive.c
+++ b/contrib/basic_archive/basic_archive.c
@@ -17,6 +17,11 @@
  * a file is successfully archived and then the system crashes before
  * a durable record of the success has been made.
  *
+ * This file also demonstrates a basic restore library implementation that
+ * is roughly equivalent to the following shell command:
+ *
+ *		cp /path/to/archivedir/%f %p
+ *
  * Copyright (c) 2022-2023, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
@@ -33,6 +38,7 @@
 #include "archive/archive_module.h"
 #include "common/int.h"
 #include "miscadmin.h"
+#include "restore/restore_module.h"
 #include "storage/copydir.h"
 #include "storage/fd.h"
 #include "utils/guc.h"
@@ -54,6 +60,16 @@ static void basic_archive_file_internal(const char *file, const char *path);
 static bool check_archive_directory(char **newval, void **extra, GucSource source);
 static bool compare_files(const char *file1, const char *file2);
 static void basic_archive_shutdown(ArchiveModuleState *state);
+static bool basic_restore_configured(RestoreModuleState *state);
+static bool basic_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool basic_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool basic_restore_file(const char *file, const char *path);
+static bool basic_restore_timeline_history_exists(RestoreModuleState *state,
+												  const char *file,
+												  const char *path);
 
 static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.startup_cb = basic_archive_startup,
@@ -62,6 +78,21 @@ static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.shutdown_cb = basic_archive_shutdown
 };
 
+static const RestoreModuleCallbacks basic_restore_callbacks = {
+	.startup_cb = NULL,
+	.restore_wal_segment_configured_cb = basic_restore_configured,
+	.restore_wal_segment_cb = basic_restore_wal_segment,
+	.restore_timeline_history_configured_cb = basic_restore_configured,
+	.restore_timeline_history_cb = basic_restore_timeline_history,
+	.timeline_history_exists_configured_cb = basic_restore_configured,
+	.timeline_history_exists_cb = basic_restore_timeline_history_exists,
+	.archive_cleanup_configured_cb = NULL,
+	.archive_cleanup_cb = NULL,
+	.recovery_end_configured_cb = NULL,
+	.recovery_end_cb = NULL,
+	.shutdown_cb = NULL
+};
+
 /*
  * _PG_init
  *
@@ -71,7 +102,7 @@ void
 _PG_init(void)
 {
 	DefineCustomStringVariable("basic_archive.archive_directory",
-							   gettext_noop("Archive file destination directory."),
+							   gettext_noop("Archive file source/destination directory."),
 							   NULL,
 							   &archive_directory,
 							   "",
@@ -93,6 +124,17 @@ _PG_archive_module_init(void)
 	return &basic_archive_callbacks;
 }
 
+/*
+ * _PG_restore_module_init
+ *
+ * Returns the module's restore callbacks.
+ */
+const RestoreModuleCallbacks *
+_PG_restore_module_init(void)
+{
+	return &basic_restore_callbacks;
+}
+
 /*
  * basic_archive_startup
  *
@@ -426,3 +468,112 @@ basic_archive_shutdown(ArchiveModuleState *state)
 	pfree(data);
 	state->private_data = NULL;
 }
+
+/*
+ * basic_restore_configured
+ *
+ * Checks that archive_directory is not blank.
+ */
+static bool
+basic_restore_configured(RestoreModuleState *state)
+{
+	return archive_directory != NULL && archive_directory[0] != '\0';
+}
+
+/*
+ * basic_restore_wal_segment
+ *
+ * Retrieves one archived WAL segment.
+ */
+static bool
+basic_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
+						  const char *lastRestartPointFileName)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_timeline_history
+ *
+ * Retrieves one timeline history file.
+ */
+static bool
+basic_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file.
+ */
+static bool
+basic_restore_file(const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive",
+					file)));
+
+	/*
+	 * If the file doesn't exist, return false to indicate that there are no
+	 * more files to restore.
+	 */
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	copy_file(archive_path, path);
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive",
+					file)));
+	return true;
+}
+
+/*
+ * basic_restore_timeline_history_exists
+ *
+ * Check whether a timeline history file is present in the archive directory.
+ */
+static bool
+basic_restore_timeline_history_exists(RestoreModuleState *state,
+									  const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("checking existence of \"%s\" via basic_archive",
+					file)));
+
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	ereport(DEBUG1,
+			(errmsg("verified existence of \"%s\" via basic_archive",
+					file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index bc1380e6f6..4e9f5002de 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -31,4 +31,9 @@ tests += {
     # which typical runningcheck users do not have (e.g. buildfarm clients).
     'runningcheck': false,
   },
+  'tap': {
+    'tests': [
+      't/001_restore.pl',
+    ],
+  },
 }
diff --git a/contrib/basic_archive/t/001_restore.pl b/contrib/basic_archive/t/001_restore.pl
new file mode 100644
index 0000000000..85b14c5113
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,44 @@
+
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# start a node
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init(has_archiving => 1, allows_streaming => 1);
+my $archive_dir = $node->archive_dir;
+$archive_dir =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
+$node->append_conf('postgresql.conf', "archive_command = ''");
+$node->append_conf('postgresql.conf', "archive_library = 'basic_archive'");
+$node->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$node->start;
+
+# backup the node
+my $backup = 'backup';
+$node->backup($backup);
+
+# generate some new WAL files
+$node->safe_psql('postgres', "CREATE TABLE test (a INT);");
+$node->safe_psql('postgres', "SELECT pg_switch_wal();");
+$node->safe_psql('postgres', "INSERT INTO test VALUES (1);");
+
+# shut down the node (this should archive all WAL files)
+$node->stop;
+
+# restore from the backup
+my $restore = PostgreSQL::Test::Cluster->new('restore');
+$restore->init_from_backup($node, $backup, has_restoring => 1, standby => 0);
+$restore->append_conf('postgresql.conf', "restore_command = ''");
+$restore->append_conf('postgresql.conf', "restore_library = 'basic_archive'");
+$restore->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$restore->start;
+
+# ensure post-backup WAL was replayed
+my $result = $restore->safe_psql("postgres", "SELECT count(*) FROM test;");
+is($result, "1", "check restore content");
+
+done_testing();
diff --git a/doc/src/sgml/archive-and-restore-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml
index f30ee591e7..56373a211b 100644
--- a/doc/src/sgml/archive-and-restore-modules.sgml
+++ b/doc/src/sgml/archive-and-restore-modules.sgml
@@ -8,15 +8,17 @@
 
  <para>
   PostgreSQL provides infrastructure to create custom modules for continuous
-  archiving (see <xref linkend="continuous-archiving"/>).  While archiving via
-  a shell command (i.e., <xref linkend="guc-archive-command"/>) is much
-  simpler, a custom archive module will often be considerably more robust and
-  performant.
+  archiving and recovery (see <xref linkend="continuous-archiving"/>).  While a
+  shell command (i.e., <xref linkend="guc-archive-command"/>,
+  <xref linkend="guc-restore-command"/>) is much simpler, a custom module will
+  often be considerably more robust and performant.
  </para>
 
  <para>
   The <filename>contrib/basic_archive</filename> module contains a working
-  example, which demonstrates some useful techniques.
+  example, which demonstrates some useful techniques.  As demonstrated in
+  <filename>basic_archive</filename> a single module may serve as both an
+  archive module and a restore module.
  </para>
 
  <sect1 id="archive-modules">
@@ -167,4 +169,348 @@ typedef void (*ArchiveShutdownCB) (ArchiveModuleState *state);
    </sect3>
   </sect2>
  </sect1>
+
+ <sect1 id="restore-modules">
+  <title>Restore Modules</title>
+  <indexterm zone="restore-modules">
+   <primary>Restore Modules</primary>
+  </indexterm>
+
+  <para>
+   When a custom <xref linkend="guc-restore-library"/> is configured,
+   PostgreSQL will use the module for restore actions.  It is
+   ultimately up to the module to decide how to accomplish each task,
+   but some recommendations are listed at
+   <xref linkend="backup-pitr-recovery"/>.
+  </para>
+
+  <para>
+   Restore modules must at least consist of an initialization function
+   (see <xref linkend="restore-module-init"/>) and the required callbacks (see
+   <xref linkend="restore-module-callbacks"/>).  However, restore modules are
+   also permitted to do much more (e.g., declare GUCs and register background
+   workers).
+  </para>
+
+  <sect2 id="restore-module-init">
+   <title>Initialization Functions</title>
+   <indexterm zone="restore-module-init">
+    <primary>_PG_restore_module_init</primary>
+   </indexterm>
+
+   <para>
+    An restore library is loaded by dynamically loading a shared library with
+    the <xref linkend="guc-restore-library"/>'s name as the library base name.
+    The normal library search path is used to locate the library.  To provide
+    the required restore module callbacks and to indicate that the library is
+    actually a restore module, it needs to provide a function named
+    <function>_PG_restore_module_init</function>.  The result of the function
+    must be a pointer to a struct of type
+    <structname>RestoreModuleCallbacks</structname>, which contains everything
+    that the core code needs to know how to make use of the restore module.
+    The return value needs to be of server lifetime, which is typically
+    achieved by defining it as a <literal>static const</literal> variable in
+    global scope.
+
+<programlisting>
+typedef struct RestoreModuleCallbacks
+{
+    RestoreStartupCB startup_cb;
+    RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+    RestoreWalSegmentCB restore_wal_segment_cb;
+    RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+    RestoreTimelineHistoryCB restore_timeline_history_cb;
+    TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+    TimelineHistoryExistsCB timeline_history_exists_cb;
+    ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+    ArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndConfiguredCB recovery_end_configured_cb;
+    RecoveryEndCB recovery_end_cb;
+    RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+</programlisting>
+
+    The <function>restore_wal_segment_configured_cb</function>,
+    <function>restore_wal_segment_cb</function>,
+    <function>restore_timeline_history_configured_cb</function>,
+    <function>restore_timeline_history_cb</function>,
+    <function>timeline_history_exists_configured_cb</function>, and
+    <function>timeline_history_exists_cb</function> callbacks are required for
+    archive recovery but optional for streaming replication.  The others are
+    always optional.
+   </para>
+  </sect2>
+
+  <sect2 id="restore-module-callbacks">
+   <title>Restore Module Callbacks</title>
+
+   <para>
+    The restore callbacks define the actual behavior of the module.  The server
+    will call them as required to execute restore actions.
+   </para>
+
+   <sect3 id="restore-module-startup">
+    <title>Startup Callback</title>
+
+    <para>
+     The <function>startup_cb</function> callback is called shortly after the
+     module is loaded.  This callback can be used to perform any additional
+     initialization required.  If the restore module has state, it can use
+     <structfield>state->private_data</structfield> to store it.
+
+<programlisting>
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+</programlisting>
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment-configured">
+    <title>Restore WAL Segment Configured Callback</title>
+    <para>
+     The <function>restore_wal_segment_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to retrieve WAL files (e.g., its configuration parameters
+     are set to valid values).  If no
+     <function>restore_wal_segment_configured_cb</function> is defined, the
+     server always assumes the module is not configured to retrieve WAL files.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve WAL files
+     by calling the <function>restore_wal_segment_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_wal_segment_cb</function> callback to retrieve WAL
+     files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment">
+    <title>Restore WAL Segment Callback</title>
+    <para>
+     The <function>restore_wal_segment_cb</function> callback is called to
+     retrieve a single archived segment of the WAL file series for archive
+     recovery or streaming replication.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state, const char *file, const char *path, const char *lastRestartPointFileName);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point.  That is the earliest
+     file that must be kept to allow a restore to be restartable, so this
+     information can be used to truncate the archive to just the minimum
+     required to support restarting from the current restore.
+     <replaceable>lastRestartPointFileName</replaceable> is typically only used
+     by warm-standby configurations (see <xref linkend="warm-standby"/>).  Note
+     that if multiple standby servers are restoring from the same archive
+     directory, you will need to ensure that you do not delete WAL files until
+     they are no longer needed by any of the servers.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history-configured">
+    <title>Restore Timeline History Configured Callback</title>
+    <para>
+     The <function>restore_timeline_history_configured_cb</function> callback
+     is called to determine whether the module is fully configured and ready to
+     accept requests to retrieve timeline history files (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>restore_timeline_history_configured_cb</function> is defined,
+     the server always assumes the module is not configured to retrieve
+     timeline history files.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve timeline
+     history files by calling the
+     <function>restore_timeline_history_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_timeline_history_cb</function> callback to retrieve
+     timeline history files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history">
+    <title>Restore Timeline History Callback</title>
+    <para>
+     The <function>restore_timeline_history_cb</function> callback is called to
+     retrieve a single archived timeline history file for archive recovery or
+     streaming replication.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists-configured">
+    <title>Timeline History Exists Configured Callback</title>
+    <para>
+     The <function>timeline_history_exists_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to determine whether a timeline history file exists in the
+     archives (e.g., its configuration parameters are set to valid values).  If
+     no <function>timeline_history_exists_configured_cb</function> is defined,
+     the server always assumes the module is not configured to check whether
+     certain timeline history files exist.
+
+<programlisting>
+typedef bool (*TimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will check whether
+     certain timeline history files are present in the archives by calling the
+     <function>timeline_history_exists_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>timeline_history_exists_cb</function> callback to check if
+     timeline history files exist in the archives.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists">
+    <title>Timeline History Exists Callback</title>
+    <para>
+     The <function>timeline_history_exists_cb</function> callback is called to
+     check whether a timeline history file is present in the archives.
+
+<programlisting>
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file is
+     present in the archives.  If the file is not available in the archives,
+     the callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the timeline history file.
+    </para>
+
+    <para>
+     Some restore modules might not have a dedicated way to determine whether a
+     timeline history file exists.  For example,
+     <varname>restore_command</varname> only tells the server how to retrieve
+     files.  In this case, the <function>timeline_history_exists_cb</function>
+     callback should copy the file to <replaceable>path</replaceable>, which
+     contains the destination's relative path (including the file name), and
+     the restore module should set
+     <structfield>state->timeline_history_exists_cb_copies</structfield> to
+     <literal>true</literal>.  It is recommended to set this flag in the
+     module's <function>startup_cb</function> callback.  This flag tells the
+     server that it should verify that the file was successfully retrieved
+     after invoking this callback.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup-configured">
+    <title>Archive Cleanup Configured Callback</title>
+    <para>
+     The <function>archive_cleanup_configured_cb</function> callback is called
+     to determine whether the module is fully configured and ready to accept
+     requests to remove old WAL files from the archives (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>archive_cleanup_configured_cb</function> is defined, the server
+     always assumes the module is not configured to remove old WAL files.
+
+<programlisting>
+typedef bool (*ArchiveCleanupConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will remove old WAL
+     files by calling the <function>archive_cleanup_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>archive_cleanup_cb</function> callback to remove old WAL files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup">
+    <title>Archive Cleanup Callback</title>
+    <para>
+     The <function>archive_cleanup_cb</function> callback is called at every
+     restart point by the checkpointer process and is intended to provide a
+     mechanism for cleaning up old archived WAL files that are no longer
+     needed by the standby server.
+
+<programlisting>
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the
+     name of the file that includes the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end-configured">
+    <title>Recovery End Configured Callback</title>
+    <para>
+     The <function>recovery_end_configured_cb</function> callback is called to
+     determine whether the module is fully configured and ready to execute its
+     <function>recovery_end_cb</function> callback once at the end of recovery
+     (e.g., its configuration parameters are set to valid values).  If no
+     <function>recovery_end_configured_cb</function> is defined, the server
+     always assumes the module is not configured to execute its
+     <function>recovery_end_cb</function> callback.
+
+<programlisting>
+typedef bool (*RecoveryEndConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will execute the
+     <function>recovery_end_cb</function> callback once at the end of recovery.
+     If <literal>false</literal> is returned, the server will not use the
+     <function>recovery_end_cb</function> callback at the end of recovery.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end">
+    <title>Recovery End Callback</title>
+    <para>
+     The <function>recovery_end_cb</function> callback is called once at the
+     end of recovery and is intended to provide a mechanism for cleanup
+     following the end of replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-shutdown">
+    <title>Shutdown Callback</title>
+    <para>
+     The <function>shutdown_cb</function> callback is called when a process
+     that has loaded the restore module exits (e.g., after an error) or the
+     value of <xref linkend="guc-restore-library"/> changes.  If no
+     <function>shutdown_cb</function> is defined, no special action is taken in
+     these situations.  If the restore module has state, this callback should
+     free it to avoid leaks.
+
+<programlisting>
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+ </programlisting>
+    </para>
+   </sect3>
+  </sect2>
+ </sect1>
 </chapter>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index be05a33205..5555f23a85 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1180,9 +1180,26 @@ SELECT * FROM pg_backup_stop(wait_for_archive => true);
    <para>
     The key part of all this is to set up a recovery configuration that
     describes how you want to recover and how far the recovery should
-    run.  The one thing that you absolutely must specify is the <varname>restore_command</varname>,
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>,
     which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    WAL file segments.
+   </para>
+
+   <para>
+    Like the <varname>archive_library</varname> parameter,
+    <varname>restore_library</varname> is a shared library.  Since such
+    libraries are written in <literal>C</literal>, creating your own may
+    require considerably more effort than writing a shell command.  However,
+    restore modules can be more performance than restoring via shell, and they
+    will have access to many useful server resources.  For more information
+    about creating a <varname>restore_library</varname>, see
+    <xref linkend="restore-modules"/>.
+   </para>
+
+   <para>
+    Like the <varname>archive_command</varname>,
+    <varname>restore_command</varname> is
     a shell command string.  It can contain <literal>%f</literal>, which is
     replaced by the name of the desired WAL file, and <literal>%p</literal>,
     which is replaced by the path name to copy the WAL file to.
@@ -1201,14 +1218,20 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
    </para>
 
    <para>
-    It is important that the command return nonzero exit status on failure.
-    The command <emphasis>will</emphasis> be called requesting files that are not
-    present in the archive; it must return nonzero when so asked.  This is not
-    an error condition.  An exception is that if the command was terminated by
+    It is important that the <varname>restore_command</varname> return nonzero
+    exit status on failure, or, if you are using a
+    <varname>restore_library</varname>, that the restore callbacks return
+    <literal>false</literal> on failure.  The command or library
+    <emphasis>will</emphasis> be called requesting files that are not
+    present in the archive; it must fail when so asked.  This is not
+    an error condition.  An exception is that if the
+    <varname>restore_command</varname> was terminated by
     a signal (other than <systemitem>SIGTERM</systemitem>, which is used as
     part of a database server shutdown) or an error by the shell (such as
     command not found), then recovery will abort and the server will not start
-    up.
+    up.  Likewise, if the restore callbacks provided by the
+    <varname>restore_library</varname> emit an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1232,7 +1255,8 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
     close as possible given the available WAL segments).  Therefore, a normal
     recovery will end with a <quote>file not found</quote> message, the exact text
     of the error message depending upon your choice of
-    <varname>restore_command</varname>.  You may also see an error message
+    <varname>restore_command</varname> or <varname>restore_library</varname>.
+    You may also see an error message
     at the start of recovery for a file named something like
     <filename>00000001.history</filename>.  This is also normal and does not
     indicate a problem in simple recovery situations; see
diff --git a/doc/src/sgml/basic-archive.sgml b/doc/src/sgml/basic-archive.sgml
index b4d43ced20..f3a5a502bd 100644
--- a/doc/src/sgml/basic-archive.sgml
+++ b/doc/src/sgml/basic-archive.sgml
@@ -8,17 +8,20 @@
  </indexterm>
 
  <para>
-  <filename>basic_archive</filename> is an example of an archive module.  This
-  module copies completed WAL segment files to the specified directory.  This
-  may not be especially useful, but it can serve as a starting point for
-  developing your own archive module.  For more information about archive
-  modules, see <xref linkend="archive-modules"/>.
+  <filename>basic_archive</filename> is an example of an archive and restore
+  module.  This module copies completed WAL segment files to or from the
+  specified directory.  This may not be especially useful, but it can serve as
+  a starting point for developing your own archive and restore modules.  For
+  more information about archive and restore modules, see\
+  <xref linkend="archive-and-restore-modules"/>.
  </para>
 
  <para>
-  In order to function, this module must be loaded via
+  For use as an archive module, this module must be loaded via
   <xref linkend="guc-archive-library"/>, and <xref linkend="guc-archive-mode"/>
-  must be enabled.
+  must be enabled.  For use as a restore module, this module must be loaded
+  via <xref linkend="guc-restore-library"/>, and recovery must be enabled (see
+  <xref linkend="runtime-config-wal-archive-recovery"/>).
  </para>
 
  <sect2 id="basic-archive-configuration-parameters">
@@ -34,11 +37,12 @@
     </term>
     <listitem>
      <para>
-      The directory where the server should copy WAL segment files.  This
-      directory must already exist.  The default is an empty string, which
-      effectively halts WAL archiving, but if <xref linkend="guc-archive-mode"/>
-      is enabled, the server will accumulate WAL segment files in the
-      expectation that a value will soon be provided.
+      The directory where the server should copy WAL segment files to or from.
+      This directory must already exist.  The default is an empty string,
+      which, when used for archiving, effectively halts WAL archival, but if
+      <xref linkend="guc-archive-mode"/> is enabled, the server will accumulate
+      WAL segment files in the expectation that a value will soon be provided.
+      When an empty string is used for recovery, restore will fail.
      </para>
     </listitem>
    </varlistentry>
@@ -46,7 +50,7 @@
 
   <para>
    These parameters must be set in <filename>postgresql.conf</filename>.
-   Typical usage might be:
+   Typical usage as an archive module might be:
   </para>
 
 <programlisting>
@@ -61,6 +65,7 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
+   When <filename>basic_archive</filename> is used as an archive module,
    Server crashes may leave temporary files with the prefix
    <filename>archtemp</filename> in the archive directory.  It is recommended to
    delete such files before restarting the server after a crash.  It is safe to
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index ecd9aa73ef..8fec106a24 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3807,7 +3807,8 @@ include_dir 'conf.d'
      recovery when the end of archived WAL is reached, but will keep trying to
      continue recovery by connecting to the sending server as specified by the
      <varname>primary_conninfo</varname> setting and/or by fetching new WAL
-     segments using <varname>restore_command</varname>.  For this mode, the
+     segments using <varname>restore_command</varname> or
+     <varname>restore_library</varname>.  For this mode, the
      parameters from this section and <xref
      linkend="runtime-config-replication-standby"/> are of interest.
      Parameters from <xref linkend="runtime-config-wal-recovery-target"/> will
@@ -3835,7 +3836,8 @@ include_dir 'conf.d'
       <listitem>
        <para>
         The local shell command to execute to retrieve an archived segment of
-        the WAL file series. This parameter is required for archive recovery,
+        the WAL file series.  Either <varname>restore_command</varname> or
+        <xref linkend="guc-restore-library"/> is required for archive recovery,
         but optional for streaming replication.
         Any <literal>%f</literal> in the string is
         replaced by the name of the file to retrieve from the archive,
@@ -3870,7 +3872,42 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
 
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>restore_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-restore-library" xreflabel="restore_library">
+      <term><varname>restore_library</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>restore_library</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        The library to use for restore actions, including retrieving archived
+        files and executing tasks at restartpoints and at recovery end.  Either
+        <xref linkend="guc-restore-command"/> or
+        <varname>restore_library</varname> is required for archive recovery,
+        but optional for streaming replication.  If this parameter is set to an
+        empty string (the default), restoring via shell is enabled, and
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, and
+        <varname>recovery_end_command</varname> are used.  If both
+        <varname>restore_library</varname> and any of
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, or
+        <varname>recovery_end_command</varname> are set, an error will be
+        raised.  Otherwise, the specified shared library is used for restoring.
+        For more information, see <xref linkend="restore-modules"/>.
+       </para>
+
+       <para>
+        This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
        </para>
       </listitem>
      </varlistentry>
@@ -3915,7 +3952,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
        </para>
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>archive_cleanup_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
        </para>
       </listitem>
      </varlistentry>
@@ -3944,7 +3984,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
        </para>
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>recovery_end_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index f180607528..957d427616 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -627,7 +627,8 @@ protocol to make nodes agree on a serializable transactional order.
    <para>
     In standby mode, the server continuously applies WAL received from the
     primary server. The standby server can read WAL from a WAL archive
-    (see <xref linkend="guc-restore-command"/>) or directly from the primary
+    (see <xref linkend="guc-restore-command"/> and
+    <xref linkend="guc-restore-library"/>) or directly from the primary
     over a TCP connection (streaming replication). The standby server will
     also attempt to restore any WAL found in the standby cluster's
     <filename>pg_wal</filename> directory. That typically happens after a server
@@ -638,8 +639,10 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     At startup, the standby begins by restoring all WAL available in the
-    archive location, calling <varname>restore_command</varname>. Once it
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s callbacks.  Once it
     reaches the end of WAL available there and <varname>restore_command</varname>
+    or <varname>restore_library</varname>
     fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
     If that fails, and streaming replication has been configured, the
     standby tries to connect to the primary server and start streaming WAL
@@ -698,7 +701,8 @@ protocol to make nodes agree on a serializable transactional order.
     server (see <xref linkend="backup-pitr-recovery"/>). Create a file
     <link linkend="file-standby-signal"><filename>standby.signal</filename></link><indexterm><primary>standby.signal</primary></indexterm>
     in the standby's cluster data
-    directory. Set <xref linkend="guc-restore-command"/> to a simple command to copy files from
+    directory. Set <xref linkend="guc-restore-command"/> or
+    <xref linkend="guc-restore-library"/> to copy files from
     the WAL archive. If you plan to have multiple standby servers for high
     availability purposes, make sure that <varname>recovery_target_timeline</varname> is set to
     <literal>latest</literal> (the default), to make the standby server follow the timeline change
@@ -707,7 +711,8 @@ protocol to make nodes agree on a serializable transactional order.
 
    <note>
      <para>
-     <xref linkend="guc-restore-command"/> should return immediately
+     <xref linkend="guc-restore-command"/> and
+     <xref linkend="guc-restore-library"/> should return immediately
      if the file does not exist; the server will retry the command again if
      necessary.
     </para>
@@ -731,8 +736,9 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     If you're using a WAL archive, its size can be minimized using the <xref
-    linkend="guc-archive-cleanup-command"/> parameter to remove files that are no
-    longer required by the standby server.
+    linkend="guc-archive-cleanup-command"/> parameter or the
+    <xref linkend="guc-restore-library"/>'s archive cleanup callbacks to remove
+    files that are no longer required by the standby server.
     The <application>pg_archivecleanup</application> utility is designed specifically to
     be used with <varname>archive_cleanup_command</varname> in typical single-standby
     configurations, see <xref linkend="pgarchivecleanup"/>.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index c4268af688..b6fa39c7cf 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -84,7 +84,7 @@
 #include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
-#include "restore/shell_restore.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
@@ -4888,18 +4888,19 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during log
-	 * shipping replication.  All files earlier than this point can be deleted
-	 * from the archive, though there is no requirement to do so.
+	 * The callback is provided the archive file cutoff point for use during
+	 * log shipping replication.  All files earlier than this point can be
+	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_recovery_end_configured())
+	if (recovery_end_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_recovery_end(lastRestartPointFname);
+		RestoreCallbacks->recovery_end_cb(restore_module_state,
+										  lastRestartPointFname);
 	}
 
 	/*
@@ -7314,18 +7315,19 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Finally, execute archive-cleanup callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during log
-	 * shipping replication.  All files earlier than this point can be deleted
-	 * from the archive, though there is no requirement to do so.
+	 * The callback is provided the archive file cutoff point for use during
+	 * log shipping replication.  All files earlier than this point can be
+	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_archive_cleanup_configured())
+	if (archive_cleanup_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_archive_cleanup(lastRestartPointFname);
+		RestoreCallbacks->archive_cleanup_cb(restore_module_state,
+											 lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index 4b45ea8753..50bcda1120 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -23,15 +23,22 @@
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
 #include "common/archive.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/startup.h"
 #include "postmaster/pgarch.h"
 #include "replication/walsender.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/lwlock.h"
+#include "utils/memutils.h"
+
+char *restoreLibrary = "";
+const RestoreModuleCallbacks *RestoreCallbacks = NULL;
+RestoreModuleState *restore_module_state;
 
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
@@ -75,15 +82,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			if (!shell_restore_file_configured())
+			if (!restore_wal_segment_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			if (!shell_restore_file_configured())
+			if (!restore_timeline_history_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			if (!shell_restore_file_configured())
+			if (!timeline_history_exists_configured())
 				goto not_available;
 			break;
 	}
@@ -175,14 +182,17 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			ret = shell_restore_wal_segment(xlogfname, xlogpath,
-											lastRestartPointFname);
+			ret = RestoreCallbacks->restore_wal_segment_cb(restore_module_state,
+														   xlogfname, xlogpath,
+														   lastRestartPointFname);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->restore_timeline_history_cb(restore_module_state,
+																xlogfname, xlogpath);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->timeline_history_exists_cb(restore_module_state,
+															   xlogfname, xlogpath);
 			break;
 	}
 
@@ -190,6 +200,16 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 
 	if (ret)
 	{
+		/*
+		 * Some restore functions might not copy the file (e.g., checking
+		 * whether a timeline history file exists), but they can set a flag to
+		 * tell us if they do.  We only need to verify file existence if this
+		 * flag is enabled.
+		 */
+		if (archive_type == ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS &&
+			!restore_module_state->timeline_history_exists_cb_copies)
+			return true;
+
 		/*
 		 * command apparently succeeded, but let's make sure the file is
 		 * really there now and has the correct size.
@@ -631,3 +651,65 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the restore callbacks into our global RestoreCallbacks.  The
+ * caller is responsible for validating the combination of library/command
+ * parameters that are set (e.g., restore_command and restore_library cannot
+ * both be set).
+ */
+void
+LoadRestoreCallbacks(void)
+{
+	RestoreModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_restore_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RestoreModuleInit)
+				load_external_function(restoreLibrary, "_PG_restore_module_init",
+									   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("restore modules have to define the symbol "
+						"_PG_restore_module_init")));
+
+	RestoreCallbacks = (*init) ();
+
+	/* restore state should be freed before calling this function */
+	Assert(restore_module_state == NULL);
+	restore_module_state = (RestoreModuleState *)
+							MemoryContextAllocZero(TopMemoryContext,
+												   sizeof(RestoreModuleState));
+
+	if (RestoreCallbacks->startup_cb != NULL)
+		RestoreCallbacks->startup_cb(restore_module_state);
+}
+
+/*
+ * Call the shutdown callback of the loaded restore module, if defined.  Also,
+ * free the restore module state if it was allocated.
+ *
+ * Processes that load restore libraries should register this via
+ * before_shmem_exit().
+ */
+void
+call_restore_module_shutdown_cb(int code, Datum arg)
+{
+	if (RestoreCallbacks != NULL &&
+		RestoreCallbacks->shutdown_cb != NULL &&
+		restore_module_state != NULL)
+		RestoreCallbacks->shutdown_cb(restore_module_state);
+
+	if (restore_module_state != NULL)
+	{
+		pfree(restore_module_state);
+		restore_module_state = NULL;
+	}
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index f0e1007f92..c6645c2413 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -50,6 +50,7 @@
 #include "postmaster/startup.h"
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
+#include "restore/restore_module.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1078,18 +1079,21 @@ validateRecoveryParameters(void)
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			(!restore_wal_segment_configured() ||
+			 !restore_timeline_history_configured() ||
+			 !timeline_history_exists_configured()))
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
+					(errmsg("specified neither primary_conninfo nor restore_command nor a configured restore_library"),
 					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
 	}
 	else
 	{
-		if (recoveryRestoreCommand == NULL ||
-			strcmp(recoveryRestoreCommand, "") == 0)
+		if (!restore_wal_segment_configured() ||
+			!restore_timeline_history_configured() ||
+			!timeline_history_exists_configured())
 			ereport(FATAL,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("must specify restore_command when standby mode is not enabled")));
+					 errmsg("must specify restore_command or a configured restore_library when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index aaad5c5228..73ff592f9f 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -45,6 +45,7 @@
 #include "postmaster/bgwriter.h"
 #include "postmaster/interrupt.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -233,6 +234,24 @@ CheckpointerMain(void)
 												 ALLOCSET_DEFAULT_SIZES);
 	MemoryContextSwitchTo(checkpointer_context);
 
+	/*
+	 * Load restore_library so that we can use its archive-cleanup callback, if
+	 * one is defined.
+	 *
+	 * We also take this opportunity to set up the before_shmem_exit hook for
+	 * the shutdown callback and to check that only one of restore_library,
+	 * archive_cleanup_command is set.  Note that we emit a WARNING upon
+	 * detecting misconfigured parameters, and we clear the callbacks so that
+	 * the archive-cleanup functionality is skipped.  We judge this
+	 * functionality to not be important enough to require blocking checkpoints
+	 * or shutting down the server when the parameters are misconfigured.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+										 archiveCleanupCommand, "archive_cleanup_command",
+										 WARNING))
+		LoadRestoreCallbacks();
+
 	/*
 	 * If an exception is encountered, processing resumes here.
 	 *
@@ -548,6 +567,10 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		bool		archive_cleanup_settings_changed = false;
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -563,6 +586,36 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		/*
+		 * If the archive-cleanup settings have changed, call the currently
+		 * loaded shutdown callback and clear all the restore callbacks.
+		 * There's presently no good way to unload a library besides restarting
+		 * the process, and there should be little harm in leaving it around,
+		 * so we just leave it loaded.
+		 */
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			archive_cleanup_settings_changed = true;
+			call_restore_module_shutdown_cb(0, (Datum) 0);
+			RestoreCallbacks = NULL;
+		}
+
+		/*
+		 * As in CheckpointerMain(), we only emit a WARNING if we detect that
+		 * both restore_library and archive_cleanup_command are set.  We do
+		 * this even if the archive-cleanup settings haven't changed to remind
+		 * the user about the misconfiguration.
+		 */
+		if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											 archiveCleanupCommand, "archive_cleanup_command",
+											 WARNING) &&
+			archive_cleanup_settings_changed)
+			LoadRestoreCallbacks();
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index 4648299ced..91229d96be 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -29,6 +29,7 @@
 #include "pgstat.h"
 #include "postmaster/interrupt.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
 #include "storage/pmsignal.h"
@@ -151,13 +152,17 @@ StartupProcShutdownHandler(SIGNAL_ARGS)
  * Re-read the config file.
  *
  * If one of the critical walreceiver options has changed, flag xlog.c
- * to restart it.
+ * to restart it.  Also, check for invalid combinations of the command/library
+ * parameters and reload the restore callbacks if necessary.
  */
 static void
 StartupRereadConfig(void)
 {
 	char	   *conninfo = pstrdup(PrimaryConnInfo);
 	char	   *slotname = pstrdup(PrimarySlotName);
+	char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+	char	   *prevRestoreCommand = pstrdup(recoveryRestoreCommand);
+	char	   *prevRecoveryEndCommand = pstrdup(recoveryEndCommand);
 	bool		tempSlot = wal_receiver_create_temp_slot;
 	bool		conninfoChanged;
 	bool		slotnameChanged;
@@ -179,6 +184,30 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	/*
+	 * If the restore settings have changed, call the currently loaded shutdown
+	 * callback and load the new callbacks.  There's presently no good way to
+	 * unload a library besides restarting the process, and there should be
+	 * little harm in leaving it around, so we just leave it loaded.
+	 */
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_restore_module_shutdown_cb(0, (Datum) 0);
+		LoadRestoreCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
@@ -288,6 +317,19 @@ StartupProcessMain(void)
 	 */
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	LoadRestoreCallbacks();
+
 	/*
 	 * Do what we came for.
 	 */
diff --git a/src/backend/restore/shell_restore.c b/src/backend/restore/shell_restore.c
index fcd5475c88..b67cd4ac48 100644
--- a/src/backend/restore/shell_restore.c
+++ b/src/backend/restore/shell_restore.c
@@ -3,7 +3,8 @@
  * shell_restore.c
  *
  * These restore functions use a user-specified shell command (e.g., the
- * restore_command GUC).
+ * restore_command GUC).  It is used as the default, but other modules may
+ * define their own restore logic.
  *
  * Copyright (c) 2023, PostgreSQL Global Development Group
  *
@@ -22,25 +23,76 @@
 #include "common/archive.h"
 #include "common/percentrepl.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static void shell_restore_startup(RestoreModuleState *state);
+static bool shell_restore_file_configured(RestoreModuleState *state);
 static bool shell_restore_file(const char *file, const char *path,
 							   const char *lastRestartPointFileName);
+static bool shell_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool shell_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool shell_archive_cleanup_configured(RestoreModuleState *state);
+static void shell_archive_cleanup(RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+static bool shell_recovery_end_configured(RestoreModuleState *state);
+static void shell_recovery_end(RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
 static void ExecuteRecoveryCommand(const char *command,
 								   const char *commandName,
 								   bool failOnSignal,
 								   uint32 wait_event_info,
 								   const char *lastRestartPointFileName);
 
+static const RestoreModuleCallbacks shell_restore_callbacks = {
+	.startup_cb = shell_restore_startup,
+	.restore_wal_segment_configured_cb = shell_restore_file_configured,
+	.restore_wal_segment_cb = shell_restore_wal_segment,
+	.restore_timeline_history_configured_cb = shell_restore_file_configured,
+	.restore_timeline_history_cb = shell_restore_timeline_history,
+	.timeline_history_exists_configured_cb = shell_restore_file_configured,
+	.timeline_history_exists_cb = shell_restore_timeline_history,
+	.archive_cleanup_configured_cb = shell_archive_cleanup_configured,
+	.archive_cleanup_cb = shell_archive_cleanup,
+	.recovery_end_configured_cb = shell_recovery_end_configured,
+	.recovery_end_cb = shell_recovery_end,
+	.shutdown_cb = NULL
+};
+
+/*
+ * shell_restore_init
+ *
+ * Returns the callbacks for restoring via shell.
+ */
+const RestoreModuleCallbacks *
+shell_restore_init(void)
+{
+	return &shell_restore_callbacks;
+}
+
+/*
+ * Indicates that our timeline_history_exists_cb only attempts to retrieve the
+ * file, so the caller should verify the file exists afterwards.
+ */
+static void
+shell_restore_startup(RestoreModuleState *state)
+{
+	state->timeline_history_exists_cb_copies = true;
+}
+
 /*
  * Attempt to execute a shell-based restore command to retrieve a WAL segment.
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_wal_segment(const char *file, const char *path,
+static bool
+shell_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
 						  const char *lastRestartPointFileName)
 {
 	return shell_restore_file(file, path, lastRestartPointFileName);
@@ -52,8 +104,9 @@ shell_restore_wal_segment(const char *file, const char *path,
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_timeline_history(const char *file, const char *path)
+static bool
+shell_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
 {
 	char		lastRestartPointFname[MAXPGPATH];
 
@@ -64,8 +117,8 @@ shell_restore_timeline_history(const char *file, const char *path)
 /*
  * Check whether restore_command is supplied.
  */
-bool
-shell_restore_file_configured(void)
+static bool
+shell_restore_file_configured(RestoreModuleState *state)
 {
 	return recoveryRestoreCommand[0] != '\0';
 }
@@ -153,8 +206,8 @@ shell_restore_file(const char *file, const char *path,
 /*
  * Check whether archive_cleanup_command is supplied.
  */
-bool
-shell_archive_cleanup_configured(void)
+static bool
+shell_archive_cleanup_configured(RestoreModuleState *state)
 {
 	return archiveCleanupCommand[0] != '\0';
 }
@@ -162,8 +215,9 @@ shell_archive_cleanup_configured(void)
 /*
  * Attempt to execute a shell-based archive cleanup command.
  */
-void
-shell_archive_cleanup(const char *lastRestartPointFileName)
+static void
+shell_archive_cleanup(RestoreModuleState *state,
+					  const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(archiveCleanupCommand, "archive_cleanup_command",
 						   false, WAIT_EVENT_ARCHIVE_CLEANUP_COMMAND,
@@ -173,8 +227,8 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 /*
  * Check whether recovery_end_command is supplied.
  */
-bool
-shell_recovery_end_configured(void)
+static bool
+shell_recovery_end_configured(RestoreModuleState *state)
 {
 	return recoveryEndCommand[0] != '\0';
 }
@@ -182,8 +236,9 @@ shell_recovery_end_configured(void)
 /*
  * Attempt to execute a shell-based end-of-recovery command.
  */
-void
-shell_recovery_end(const char *lastRestartPointFileName)
+static void
+shell_recovery_end(RestoreModuleState *state,
+				   const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(recoveryEndCommand, "recovery_end_command", true,
 						   WAIT_EVENT_RECOVERY_END_COMMAND,
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1c0583fe26..a42819399b 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -63,6 +63,7 @@
 #include "replication/logicallauncher.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -3788,6 +3789,16 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"restore_library", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
+			gettext_noop("Sets the library that will be called for restore actions."),
+			NULL
+		},
+		&restoreLibrary,
+		"",
+		NULL, NULL, NULL
+	},
+
 	{
 		{"archive_cleanup_command", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
 			gettext_noop("Sets the shell command that will be executed at every restart point."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index d06074b86f..53757fea5f 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -270,6 +270,7 @@
 				# placeholders: %p = path of file to restore
 				#               %f = file name only
 				# e.g. 'cp /mnt/server/archivedir/%f %p'
+#restore_library = ''		# library to use for restore actions
 #archive_cleanup_command = ''	# command to execute at every restartpoint
 #recovery_end_command = ''	# command to execute at completion of recovery
 
diff --git a/src/include/restore/restore_module.h b/src/include/restore/restore_module.h
new file mode 100644
index 0000000000..c0aef33ba5
--- /dev/null
+++ b/src/include/restore/restore_module.h
@@ -0,0 +1,142 @@
+/*-------------------------------------------------------------------------
+ *
+ * restore_module.h
+ *		Exports for restore modules.
+ *
+ * Copyright (c) 2023, PostgreSQL Global Development Group
+ *
+ * src/include/restore/restore_module.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef _RESTORE_MODULE_H
+#define _RESTORE_MODULE_H
+
+/*
+ * The value of the restore_library GUC.
+ */
+extern PGDLLIMPORT char *restoreLibrary;
+
+typedef struct RestoreModuleState
+{
+	/*
+	 * Private data pointer for use by a restore module.  This can be used to
+	 * store state for the module that will be passed to each of its callbacks.
+	 */
+	void	   *private_data;
+
+	/*
+	 * Indicates whether the callback that checks for timeline existence merely
+	 * copies the file.  If set to true, the server will verify that the
+	 * timeline history file exists after the callback returns.
+	 */
+	bool		timeline_history_exists_cb_copies;
+} RestoreModuleState;
+
+/*
+ * Restore module callbacks
+ *
+ * These callback functions should be defined by restore libraries and returned
+ * via _PG_restore_module_init().  For more information about the purpose of
+ * each callback, refer to the restore modules documentation.
+ */
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state,
+									 const char *file, const char *path,
+									 const char *lastRestartPointFileName);
+typedef bool (*RestoreTimelineHistoryConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state,
+										  const char *file, const char *path);
+typedef bool (*TimelineHistoryExistsConfiguredCB) (RestoreModuleState *state);
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state,
+										 const char *file, const char *path);
+typedef bool (*ArchiveCleanupConfiguredCB) (RestoreModuleState *state);
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+typedef bool (*RecoveryEndConfiguredCB) (RestoreModuleState *state);
+typedef void (*RecoveryEndCB) (RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+
+typedef struct RestoreModuleCallbacks
+{
+	RestoreStartupCB startup_cb;
+	RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+	RestoreWalSegmentCB restore_wal_segment_cb;
+	RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+	RestoreTimelineHistoryCB restore_timeline_history_cb;
+	TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+	TimelineHistoryExistsCB timeline_history_exists_cb;
+	ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+	ArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndConfiguredCB recovery_end_configured_cb;
+	RecoveryEndCB recovery_end_cb;
+	RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+
+/*
+ * Type of the shared library symbol _PG_restore_module_init that is looked up
+ * when loading a restore library.
+ */
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+
+extern PGDLLEXPORT const RestoreModuleCallbacks *_PG_restore_module_init(void);
+
+extern void LoadRestoreCallbacks(void);
+extern void call_restore_module_shutdown_cb(int code, Datum arg);
+
+extern const RestoreModuleCallbacks *RestoreCallbacks;
+extern RestoreModuleState *restore_module_state;
+
+/*
+ * The following *_configured() functions are convenient wrappers around the
+ * *_configured_cb() callback functions with additional defensive NULL checks.
+ */
+
+static inline bool
+restore_wal_segment_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		   RestoreCallbacks->restore_wal_segment_configured_cb != NULL &&
+		   RestoreCallbacks->restore_wal_segment_cb != NULL &&
+		   RestoreCallbacks->restore_wal_segment_configured_cb(restore_module_state);
+}
+
+static inline bool
+restore_timeline_history_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		   RestoreCallbacks->restore_timeline_history_configured_cb != NULL &&
+		   RestoreCallbacks->restore_timeline_history_cb != NULL &&
+		   RestoreCallbacks->restore_timeline_history_configured_cb(restore_module_state);
+}
+
+static inline bool
+timeline_history_exists_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		   RestoreCallbacks->timeline_history_exists_configured_cb != NULL &&
+		   RestoreCallbacks->timeline_history_exists_cb != NULL &&
+		   RestoreCallbacks->timeline_history_exists_configured_cb(restore_module_state);
+}
+
+static inline bool
+archive_cleanup_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		   RestoreCallbacks->archive_cleanup_configured_cb != NULL &&
+		   RestoreCallbacks->archive_cleanup_cb != NULL &&
+		   RestoreCallbacks->archive_cleanup_configured_cb(restore_module_state);
+}
+
+static inline bool
+recovery_end_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		   RestoreCallbacks->recovery_end_configured_cb != NULL &&
+		   RestoreCallbacks->recovery_end_cb != NULL &&
+		   RestoreCallbacks->recovery_end_configured_cb(restore_module_state);
+}
+
+#endif							/* _RESTORE_MODULE_H */
diff --git a/src/include/restore/shell_restore.h b/src/include/restore/shell_restore.h
index 570588e493..1eeae51d81 100644
--- a/src/include/restore/shell_restore.h
+++ b/src/include/restore/shell_restore.h
@@ -12,15 +12,13 @@
 #ifndef _SHELL_RESTORE_H
 #define _SHELL_RESTORE_H
 
-extern bool shell_restore_file_configured(void);
-extern bool shell_restore_wal_segment(const char *file, const char *path,
-									  const char *lastRestartPointFileName);
-extern bool shell_restore_timeline_history(const char *file, const char *path);
+#include "restore/restore_module.h"
 
-extern bool shell_archive_cleanup_configured(void);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-
-extern bool shell_recovery_end_configured(void);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Since the logic for restoring via shell commands is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern const RestoreModuleCallbacks *shell_restore_init(void);
 
 #endif							/* _SHELL_RESTORE_H */
-- 
2.25.1


--YiEDa0DAkWCtVeE4--





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

* [PATCH v13 7/7] introduce restore_library
@ 2023-02-16 06:06  Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 28+ messages in thread

From: Nathan Bossart @ 2023-02-16 06:06 UTC (permalink / raw)

---
 contrib/basic_archive/Makefile                |   1 +
 contrib/basic_archive/basic_archive.c         | 153 +++++++-
 contrib/basic_archive/meson.build             |   5 +
 contrib/basic_archive/t/001_restore.pl        |  44 +++
 doc/src/sgml/archive-and-restore-modules.sgml | 356 +++++++++++++++++-
 doc/src/sgml/backup.sgml                      |  40 +-
 doc/src/sgml/basic-archive.sgml               |  31 +-
 doc/src/sgml/config.sgml                      |  53 ++-
 doc/src/sgml/high-availability.sgml           |  18 +-
 src/backend/access/transam/xlog.c             |  28 +-
 src/backend/access/transam/xlogarchive.c      |  96 ++++-
 src/backend/access/transam/xlogrecovery.c     |  14 +-
 src/backend/postmaster/checkpointer.c         |  53 +++
 src/backend/postmaster/startup.c              |  44 ++-
 src/backend/restore/shell_restore.c           |  85 ++++-
 src/backend/utils/misc/guc_tables.c           |  11 +
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/restore/restore_module.h          | 142 +++++++
 src/include/restore/shell_restore.h           |  16 +-
 19 files changed, 1103 insertions(+), 88 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl
 create mode 100644 src/include/restore/restore_module.h

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 55d299d650..d12d45e0d2 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -5,6 +5,7 @@ PGFILEDESC = "basic_archive - basic archive module"
 
 REGRESS = basic_archive
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.conf
+TAP_TESTS = 1
 # Disabled because these tests require "shared_preload_libraries=basic_archive",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index cd852888ce..b04d83da4c 100644
--- a/contrib/basic_archive/basic_archive.c
+++ b/contrib/basic_archive/basic_archive.c
@@ -17,6 +17,11 @@
  * a file is successfully archived and then the system crashes before
  * a durable record of the success has been made.
  *
+ * This file also demonstrates a basic restore library implementation that
+ * is roughly equivalent to the following shell command:
+ *
+ *		cp /path/to/archivedir/%f %p
+ *
  * Copyright (c) 2022-2023, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
@@ -33,6 +38,7 @@
 #include "archive/archive_module.h"
 #include "common/int.h"
 #include "miscadmin.h"
+#include "restore/restore_module.h"
 #include "storage/copydir.h"
 #include "storage/fd.h"
 #include "utils/guc.h"
@@ -54,6 +60,16 @@ static void basic_archive_file_internal(const char *file, const char *path);
 static bool check_archive_directory(char **newval, void **extra, GucSource source);
 static bool compare_files(const char *file1, const char *file2);
 static void basic_archive_shutdown(ArchiveModuleState *state);
+static bool basic_restore_configured(RestoreModuleState *state);
+static bool basic_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool basic_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool basic_restore_file(const char *file, const char *path);
+static bool basic_restore_timeline_history_exists(RestoreModuleState *state,
+												  const char *file,
+												  const char *path);
 
 static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.startup_cb = basic_archive_startup,
@@ -62,6 +78,21 @@ static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.shutdown_cb = basic_archive_shutdown
 };
 
+static const RestoreModuleCallbacks basic_restore_callbacks = {
+	.startup_cb = NULL,
+	.restore_wal_segment_configured_cb = basic_restore_configured,
+	.restore_wal_segment_cb = basic_restore_wal_segment,
+	.restore_timeline_history_configured_cb = basic_restore_configured,
+	.restore_timeline_history_cb = basic_restore_timeline_history,
+	.timeline_history_exists_configured_cb = basic_restore_configured,
+	.timeline_history_exists_cb = basic_restore_timeline_history_exists,
+	.archive_cleanup_configured_cb = NULL,
+	.archive_cleanup_cb = NULL,
+	.recovery_end_configured_cb = NULL,
+	.recovery_end_cb = NULL,
+	.shutdown_cb = NULL
+};
+
 /*
  * _PG_init
  *
@@ -71,7 +102,7 @@ void
 _PG_init(void)
 {
 	DefineCustomStringVariable("basic_archive.archive_directory",
-							   gettext_noop("Archive file destination directory."),
+							   gettext_noop("Archive file source/destination directory."),
 							   NULL,
 							   &archive_directory,
 							   "",
@@ -93,6 +124,17 @@ _PG_archive_module_init(void)
 	return &basic_archive_callbacks;
 }
 
+/*
+ * _PG_restore_module_init
+ *
+ * Returns the module's restore callbacks.
+ */
+const RestoreModuleCallbacks *
+_PG_restore_module_init(void)
+{
+	return &basic_restore_callbacks;
+}
+
 /*
  * basic_archive_startup
  *
@@ -426,3 +468,112 @@ basic_archive_shutdown(ArchiveModuleState *state)
 	pfree(data);
 	state->private_data = NULL;
 }
+
+/*
+ * basic_restore_configured
+ *
+ * Checks that archive_directory is not blank.
+ */
+static bool
+basic_restore_configured(RestoreModuleState *state)
+{
+	return archive_directory != NULL && archive_directory[0] != '\0';
+}
+
+/*
+ * basic_restore_wal_segment
+ *
+ * Retrieves one archived WAL segment.
+ */
+static bool
+basic_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
+						  const char *lastRestartPointFileName)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_timeline_history
+ *
+ * Retrieves one timeline history file.
+ */
+static bool
+basic_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file.
+ */
+static bool
+basic_restore_file(const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive",
+					file)));
+
+	/*
+	 * If the file doesn't exist, return false to indicate that there are no
+	 * more files to restore.
+	 */
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	copy_file(archive_path, path);
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive",
+					file)));
+	return true;
+}
+
+/*
+ * basic_restore_timeline_history_exists
+ *
+ * Check whether a timeline history file is present in the archive directory.
+ */
+static bool
+basic_restore_timeline_history_exists(RestoreModuleState *state,
+									  const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("checking existence of \"%s\" via basic_archive",
+					file)));
+
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	ereport(DEBUG1,
+			(errmsg("verified existence of \"%s\" via basic_archive",
+					file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index bc1380e6f6..4e9f5002de 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -31,4 +31,9 @@ tests += {
     # which typical runningcheck users do not have (e.g. buildfarm clients).
     'runningcheck': false,
   },
+  'tap': {
+    'tests': [
+      't/001_restore.pl',
+    ],
+  },
 }
diff --git a/contrib/basic_archive/t/001_restore.pl b/contrib/basic_archive/t/001_restore.pl
new file mode 100644
index 0000000000..85b14c5113
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,44 @@
+
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# start a node
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init(has_archiving => 1, allows_streaming => 1);
+my $archive_dir = $node->archive_dir;
+$archive_dir =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
+$node->append_conf('postgresql.conf', "archive_command = ''");
+$node->append_conf('postgresql.conf', "archive_library = 'basic_archive'");
+$node->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$node->start;
+
+# backup the node
+my $backup = 'backup';
+$node->backup($backup);
+
+# generate some new WAL files
+$node->safe_psql('postgres', "CREATE TABLE test (a INT);");
+$node->safe_psql('postgres', "SELECT pg_switch_wal();");
+$node->safe_psql('postgres', "INSERT INTO test VALUES (1);");
+
+# shut down the node (this should archive all WAL files)
+$node->stop;
+
+# restore from the backup
+my $restore = PostgreSQL::Test::Cluster->new('restore');
+$restore->init_from_backup($node, $backup, has_restoring => 1, standby => 0);
+$restore->append_conf('postgresql.conf', "restore_command = ''");
+$restore->append_conf('postgresql.conf', "restore_library = 'basic_archive'");
+$restore->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$restore->start;
+
+# ensure post-backup WAL was replayed
+my $result = $restore->safe_psql("postgres", "SELECT count(*) FROM test;");
+is($result, "1", "check restore content");
+
+done_testing();
diff --git a/doc/src/sgml/archive-and-restore-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml
index f30ee591e7..56373a211b 100644
--- a/doc/src/sgml/archive-and-restore-modules.sgml
+++ b/doc/src/sgml/archive-and-restore-modules.sgml
@@ -8,15 +8,17 @@
 
  <para>
   PostgreSQL provides infrastructure to create custom modules for continuous
-  archiving (see <xref linkend="continuous-archiving"/>).  While archiving via
-  a shell command (i.e., <xref linkend="guc-archive-command"/>) is much
-  simpler, a custom archive module will often be considerably more robust and
-  performant.
+  archiving and recovery (see <xref linkend="continuous-archiving"/>).  While a
+  shell command (i.e., <xref linkend="guc-archive-command"/>,
+  <xref linkend="guc-restore-command"/>) is much simpler, a custom module will
+  often be considerably more robust and performant.
  </para>
 
  <para>
   The <filename>contrib/basic_archive</filename> module contains a working
-  example, which demonstrates some useful techniques.
+  example, which demonstrates some useful techniques.  As demonstrated in
+  <filename>basic_archive</filename> a single module may serve as both an
+  archive module and a restore module.
  </para>
 
  <sect1 id="archive-modules">
@@ -167,4 +169,348 @@ typedef void (*ArchiveShutdownCB) (ArchiveModuleState *state);
    </sect3>
   </sect2>
  </sect1>
+
+ <sect1 id="restore-modules">
+  <title>Restore Modules</title>
+  <indexterm zone="restore-modules">
+   <primary>Restore Modules</primary>
+  </indexterm>
+
+  <para>
+   When a custom <xref linkend="guc-restore-library"/> is configured,
+   PostgreSQL will use the module for restore actions.  It is
+   ultimately up to the module to decide how to accomplish each task,
+   but some recommendations are listed at
+   <xref linkend="backup-pitr-recovery"/>.
+  </para>
+
+  <para>
+   Restore modules must at least consist of an initialization function
+   (see <xref linkend="restore-module-init"/>) and the required callbacks (see
+   <xref linkend="restore-module-callbacks"/>).  However, restore modules are
+   also permitted to do much more (e.g., declare GUCs and register background
+   workers).
+  </para>
+
+  <sect2 id="restore-module-init">
+   <title>Initialization Functions</title>
+   <indexterm zone="restore-module-init">
+    <primary>_PG_restore_module_init</primary>
+   </indexterm>
+
+   <para>
+    An restore library is loaded by dynamically loading a shared library with
+    the <xref linkend="guc-restore-library"/>'s name as the library base name.
+    The normal library search path is used to locate the library.  To provide
+    the required restore module callbacks and to indicate that the library is
+    actually a restore module, it needs to provide a function named
+    <function>_PG_restore_module_init</function>.  The result of the function
+    must be a pointer to a struct of type
+    <structname>RestoreModuleCallbacks</structname>, which contains everything
+    that the core code needs to know how to make use of the restore module.
+    The return value needs to be of server lifetime, which is typically
+    achieved by defining it as a <literal>static const</literal> variable in
+    global scope.
+
+<programlisting>
+typedef struct RestoreModuleCallbacks
+{
+    RestoreStartupCB startup_cb;
+    RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+    RestoreWalSegmentCB restore_wal_segment_cb;
+    RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+    RestoreTimelineHistoryCB restore_timeline_history_cb;
+    TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+    TimelineHistoryExistsCB timeline_history_exists_cb;
+    ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+    ArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndConfiguredCB recovery_end_configured_cb;
+    RecoveryEndCB recovery_end_cb;
+    RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+</programlisting>
+
+    The <function>restore_wal_segment_configured_cb</function>,
+    <function>restore_wal_segment_cb</function>,
+    <function>restore_timeline_history_configured_cb</function>,
+    <function>restore_timeline_history_cb</function>,
+    <function>timeline_history_exists_configured_cb</function>, and
+    <function>timeline_history_exists_cb</function> callbacks are required for
+    archive recovery but optional for streaming replication.  The others are
+    always optional.
+   </para>
+  </sect2>
+
+  <sect2 id="restore-module-callbacks">
+   <title>Restore Module Callbacks</title>
+
+   <para>
+    The restore callbacks define the actual behavior of the module.  The server
+    will call them as required to execute restore actions.
+   </para>
+
+   <sect3 id="restore-module-startup">
+    <title>Startup Callback</title>
+
+    <para>
+     The <function>startup_cb</function> callback is called shortly after the
+     module is loaded.  This callback can be used to perform any additional
+     initialization required.  If the restore module has state, it can use
+     <structfield>state->private_data</structfield> to store it.
+
+<programlisting>
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+</programlisting>
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment-configured">
+    <title>Restore WAL Segment Configured Callback</title>
+    <para>
+     The <function>restore_wal_segment_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to retrieve WAL files (e.g., its configuration parameters
+     are set to valid values).  If no
+     <function>restore_wal_segment_configured_cb</function> is defined, the
+     server always assumes the module is not configured to retrieve WAL files.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve WAL files
+     by calling the <function>restore_wal_segment_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_wal_segment_cb</function> callback to retrieve WAL
+     files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment">
+    <title>Restore WAL Segment Callback</title>
+    <para>
+     The <function>restore_wal_segment_cb</function> callback is called to
+     retrieve a single archived segment of the WAL file series for archive
+     recovery or streaming replication.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state, const char *file, const char *path, const char *lastRestartPointFileName);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point.  That is the earliest
+     file that must be kept to allow a restore to be restartable, so this
+     information can be used to truncate the archive to just the minimum
+     required to support restarting from the current restore.
+     <replaceable>lastRestartPointFileName</replaceable> is typically only used
+     by warm-standby configurations (see <xref linkend="warm-standby"/>).  Note
+     that if multiple standby servers are restoring from the same archive
+     directory, you will need to ensure that you do not delete WAL files until
+     they are no longer needed by any of the servers.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history-configured">
+    <title>Restore Timeline History Configured Callback</title>
+    <para>
+     The <function>restore_timeline_history_configured_cb</function> callback
+     is called to determine whether the module is fully configured and ready to
+     accept requests to retrieve timeline history files (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>restore_timeline_history_configured_cb</function> is defined,
+     the server always assumes the module is not configured to retrieve
+     timeline history files.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve timeline
+     history files by calling the
+     <function>restore_timeline_history_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_timeline_history_cb</function> callback to retrieve
+     timeline history files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history">
+    <title>Restore Timeline History Callback</title>
+    <para>
+     The <function>restore_timeline_history_cb</function> callback is called to
+     retrieve a single archived timeline history file for archive recovery or
+     streaming replication.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists-configured">
+    <title>Timeline History Exists Configured Callback</title>
+    <para>
+     The <function>timeline_history_exists_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to determine whether a timeline history file exists in the
+     archives (e.g., its configuration parameters are set to valid values).  If
+     no <function>timeline_history_exists_configured_cb</function> is defined,
+     the server always assumes the module is not configured to check whether
+     certain timeline history files exist.
+
+<programlisting>
+typedef bool (*TimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will check whether
+     certain timeline history files are present in the archives by calling the
+     <function>timeline_history_exists_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>timeline_history_exists_cb</function> callback to check if
+     timeline history files exist in the archives.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists">
+    <title>Timeline History Exists Callback</title>
+    <para>
+     The <function>timeline_history_exists_cb</function> callback is called to
+     check whether a timeline history file is present in the archives.
+
+<programlisting>
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file is
+     present in the archives.  If the file is not available in the archives,
+     the callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the timeline history file.
+    </para>
+
+    <para>
+     Some restore modules might not have a dedicated way to determine whether a
+     timeline history file exists.  For example,
+     <varname>restore_command</varname> only tells the server how to retrieve
+     files.  In this case, the <function>timeline_history_exists_cb</function>
+     callback should copy the file to <replaceable>path</replaceable>, which
+     contains the destination's relative path (including the file name), and
+     the restore module should set
+     <structfield>state->timeline_history_exists_cb_copies</structfield> to
+     <literal>true</literal>.  It is recommended to set this flag in the
+     module's <function>startup_cb</function> callback.  This flag tells the
+     server that it should verify that the file was successfully retrieved
+     after invoking this callback.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup-configured">
+    <title>Archive Cleanup Configured Callback</title>
+    <para>
+     The <function>archive_cleanup_configured_cb</function> callback is called
+     to determine whether the module is fully configured and ready to accept
+     requests to remove old WAL files from the archives (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>archive_cleanup_configured_cb</function> is defined, the server
+     always assumes the module is not configured to remove old WAL files.
+
+<programlisting>
+typedef bool (*ArchiveCleanupConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will remove old WAL
+     files by calling the <function>archive_cleanup_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>archive_cleanup_cb</function> callback to remove old WAL files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup">
+    <title>Archive Cleanup Callback</title>
+    <para>
+     The <function>archive_cleanup_cb</function> callback is called at every
+     restart point by the checkpointer process and is intended to provide a
+     mechanism for cleaning up old archived WAL files that are no longer
+     needed by the standby server.
+
+<programlisting>
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the
+     name of the file that includes the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end-configured">
+    <title>Recovery End Configured Callback</title>
+    <para>
+     The <function>recovery_end_configured_cb</function> callback is called to
+     determine whether the module is fully configured and ready to execute its
+     <function>recovery_end_cb</function> callback once at the end of recovery
+     (e.g., its configuration parameters are set to valid values).  If no
+     <function>recovery_end_configured_cb</function> is defined, the server
+     always assumes the module is not configured to execute its
+     <function>recovery_end_cb</function> callback.
+
+<programlisting>
+typedef bool (*RecoveryEndConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will execute the
+     <function>recovery_end_cb</function> callback once at the end of recovery.
+     If <literal>false</literal> is returned, the server will not use the
+     <function>recovery_end_cb</function> callback at the end of recovery.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end">
+    <title>Recovery End Callback</title>
+    <para>
+     The <function>recovery_end_cb</function> callback is called once at the
+     end of recovery and is intended to provide a mechanism for cleanup
+     following the end of replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-shutdown">
+    <title>Shutdown Callback</title>
+    <para>
+     The <function>shutdown_cb</function> callback is called when a process
+     that has loaded the restore module exits (e.g., after an error) or the
+     value of <xref linkend="guc-restore-library"/> changes.  If no
+     <function>shutdown_cb</function> is defined, no special action is taken in
+     these situations.  If the restore module has state, this callback should
+     free it to avoid leaks.
+
+<programlisting>
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+ </programlisting>
+    </para>
+   </sect3>
+  </sect2>
+ </sect1>
 </chapter>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index be05a33205..5555f23a85 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1180,9 +1180,26 @@ SELECT * FROM pg_backup_stop(wait_for_archive => true);
    <para>
     The key part of all this is to set up a recovery configuration that
     describes how you want to recover and how far the recovery should
-    run.  The one thing that you absolutely must specify is the <varname>restore_command</varname>,
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>,
     which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    WAL file segments.
+   </para>
+
+   <para>
+    Like the <varname>archive_library</varname> parameter,
+    <varname>restore_library</varname> is a shared library.  Since such
+    libraries are written in <literal>C</literal>, creating your own may
+    require considerably more effort than writing a shell command.  However,
+    restore modules can be more performance than restoring via shell, and they
+    will have access to many useful server resources.  For more information
+    about creating a <varname>restore_library</varname>, see
+    <xref linkend="restore-modules"/>.
+   </para>
+
+   <para>
+    Like the <varname>archive_command</varname>,
+    <varname>restore_command</varname> is
     a shell command string.  It can contain <literal>%f</literal>, which is
     replaced by the name of the desired WAL file, and <literal>%p</literal>,
     which is replaced by the path name to copy the WAL file to.
@@ -1201,14 +1218,20 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
    </para>
 
    <para>
-    It is important that the command return nonzero exit status on failure.
-    The command <emphasis>will</emphasis> be called requesting files that are not
-    present in the archive; it must return nonzero when so asked.  This is not
-    an error condition.  An exception is that if the command was terminated by
+    It is important that the <varname>restore_command</varname> return nonzero
+    exit status on failure, or, if you are using a
+    <varname>restore_library</varname>, that the restore callbacks return
+    <literal>false</literal> on failure.  The command or library
+    <emphasis>will</emphasis> be called requesting files that are not
+    present in the archive; it must fail when so asked.  This is not
+    an error condition.  An exception is that if the
+    <varname>restore_command</varname> was terminated by
     a signal (other than <systemitem>SIGTERM</systemitem>, which is used as
     part of a database server shutdown) or an error by the shell (such as
     command not found), then recovery will abort and the server will not start
-    up.
+    up.  Likewise, if the restore callbacks provided by the
+    <varname>restore_library</varname> emit an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1232,7 +1255,8 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
     close as possible given the available WAL segments).  Therefore, a normal
     recovery will end with a <quote>file not found</quote> message, the exact text
     of the error message depending upon your choice of
-    <varname>restore_command</varname>.  You may also see an error message
+    <varname>restore_command</varname> or <varname>restore_library</varname>.
+    You may also see an error message
     at the start of recovery for a file named something like
     <filename>00000001.history</filename>.  This is also normal and does not
     indicate a problem in simple recovery situations; see
diff --git a/doc/src/sgml/basic-archive.sgml b/doc/src/sgml/basic-archive.sgml
index b4d43ced20..f3a5a502bd 100644
--- a/doc/src/sgml/basic-archive.sgml
+++ b/doc/src/sgml/basic-archive.sgml
@@ -8,17 +8,20 @@
  </indexterm>
 
  <para>
-  <filename>basic_archive</filename> is an example of an archive module.  This
-  module copies completed WAL segment files to the specified directory.  This
-  may not be especially useful, but it can serve as a starting point for
-  developing your own archive module.  For more information about archive
-  modules, see <xref linkend="archive-modules"/>.
+  <filename>basic_archive</filename> is an example of an archive and restore
+  module.  This module copies completed WAL segment files to or from the
+  specified directory.  This may not be especially useful, but it can serve as
+  a starting point for developing your own archive and restore modules.  For
+  more information about archive and restore modules, see\
+  <xref linkend="archive-and-restore-modules"/>.
  </para>
 
  <para>
-  In order to function, this module must be loaded via
+  For use as an archive module, this module must be loaded via
   <xref linkend="guc-archive-library"/>, and <xref linkend="guc-archive-mode"/>
-  must be enabled.
+  must be enabled.  For use as a restore module, this module must be loaded
+  via <xref linkend="guc-restore-library"/>, and recovery must be enabled (see
+  <xref linkend="runtime-config-wal-archive-recovery"/>).
  </para>
 
  <sect2 id="basic-archive-configuration-parameters">
@@ -34,11 +37,12 @@
     </term>
     <listitem>
      <para>
-      The directory where the server should copy WAL segment files.  This
-      directory must already exist.  The default is an empty string, which
-      effectively halts WAL archiving, but if <xref linkend="guc-archive-mode"/>
-      is enabled, the server will accumulate WAL segment files in the
-      expectation that a value will soon be provided.
+      The directory where the server should copy WAL segment files to or from.
+      This directory must already exist.  The default is an empty string,
+      which, when used for archiving, effectively halts WAL archival, but if
+      <xref linkend="guc-archive-mode"/> is enabled, the server will accumulate
+      WAL segment files in the expectation that a value will soon be provided.
+      When an empty string is used for recovery, restore will fail.
      </para>
     </listitem>
    </varlistentry>
@@ -46,7 +50,7 @@
 
   <para>
    These parameters must be set in <filename>postgresql.conf</filename>.
-   Typical usage might be:
+   Typical usage as an archive module might be:
   </para>
 
 <programlisting>
@@ -61,6 +65,7 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
+   When <filename>basic_archive</filename> is used as an archive module,
    Server crashes may leave temporary files with the prefix
    <filename>archtemp</filename> in the archive directory.  It is recommended to
    delete such files before restarting the server after a crash.  It is safe to
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index e5c41cc6c6..288d58f48a 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3807,7 +3807,8 @@ include_dir 'conf.d'
      recovery when the end of archived WAL is reached, but will keep trying to
      continue recovery by connecting to the sending server as specified by the
      <varname>primary_conninfo</varname> setting and/or by fetching new WAL
-     segments using <varname>restore_command</varname>.  For this mode, the
+     segments using <varname>restore_command</varname> or
+     <varname>restore_library</varname>.  For this mode, the
      parameters from this section and <xref
      linkend="runtime-config-replication-standby"/> are of interest.
      Parameters from <xref linkend="runtime-config-wal-recovery-target"/> will
@@ -3835,7 +3836,8 @@ include_dir 'conf.d'
       <listitem>
        <para>
         The local shell command to execute to retrieve an archived segment of
-        the WAL file series. This parameter is required for archive recovery,
+        the WAL file series.  Either <varname>restore_command</varname> or
+        <xref linkend="guc-restore-library"/> is required for archive recovery,
         but optional for streaming replication.
         Any <literal>%f</literal> in the string is
         replaced by the name of the file to retrieve from the archive,
@@ -3870,7 +3872,42 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
 
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>restore_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-restore-library" xreflabel="restore_library">
+      <term><varname>restore_library</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>restore_library</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        The library to use for restore actions, including retrieving archived
+        files and executing tasks at restartpoints and at recovery end.  Either
+        <xref linkend="guc-restore-command"/> or
+        <varname>restore_library</varname> is required for archive recovery,
+        but optional for streaming replication.  If this parameter is set to an
+        empty string (the default), restoring via shell is enabled, and
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, and
+        <varname>recovery_end_command</varname> are used.  If both
+        <varname>restore_library</varname> and any of
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, or
+        <varname>recovery_end_command</varname> are set, an error will be
+        raised.  Otherwise, the specified shared library is used for restoring.
+        For more information, see <xref linkend="restore-modules"/>.
+       </para>
+
+       <para>
+        This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
        </para>
       </listitem>
      </varlistentry>
@@ -3915,7 +3952,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
        </para>
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>archive_cleanup_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
        </para>
       </listitem>
      </varlistentry>
@@ -3944,7 +3984,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
        </para>
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>recovery_end_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index 9d0deaeeb8..094140cecc 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -627,7 +627,8 @@ protocol to make nodes agree on a serializable transactional order.
    <para>
     In standby mode, the server continuously applies WAL received from the
     primary server. The standby server can read WAL from a WAL archive
-    (see <xref linkend="guc-restore-command"/>) or directly from the primary
+    (see <xref linkend="guc-restore-command"/> and
+    <xref linkend="guc-restore-library"/>) or directly from the primary
     over a TCP connection (streaming replication). The standby server will
     also attempt to restore any WAL found in the standby cluster's
     <filename>pg_wal</filename> directory. That typically happens after a server
@@ -638,8 +639,10 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     At startup, the standby begins by restoring all WAL available in the
-    archive location, calling <varname>restore_command</varname>. Once it
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s callbacks.  Once it
     reaches the end of WAL available there and <varname>restore_command</varname>
+    or <varname>restore_library</varname>
     fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
     If that fails, and streaming replication has been configured, the
     standby tries to connect to the primary server and start streaming WAL
@@ -698,7 +701,8 @@ protocol to make nodes agree on a serializable transactional order.
     server (see <xref linkend="backup-pitr-recovery"/>). Create a file
     <link linkend="file-standby-signal"><filename>standby.signal</filename></link><indexterm><primary>standby.signal</primary></indexterm>
     in the standby's cluster data
-    directory. Set <xref linkend="guc-restore-command"/> to a simple command to copy files from
+    directory. Set <xref linkend="guc-restore-command"/> or
+    <xref linkend="guc-restore-library"/> to copy files from
     the WAL archive. If you plan to have multiple standby servers for high
     availability purposes, make sure that <varname>recovery_target_timeline</varname> is set to
     <literal>latest</literal> (the default), to make the standby server follow the timeline change
@@ -707,7 +711,8 @@ protocol to make nodes agree on a serializable transactional order.
 
    <note>
      <para>
-     <xref linkend="guc-restore-command"/> should return immediately
+     <xref linkend="guc-restore-command"/> and
+     <xref linkend="guc-restore-library"/> should return immediately
      if the file does not exist; the server will retry the command again if
      necessary.
     </para>
@@ -731,8 +736,9 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     If you're using a WAL archive, its size can be minimized using the <xref
-    linkend="guc-archive-cleanup-command"/> parameter to remove files that are no
-    longer required by the standby server.
+    linkend="guc-archive-cleanup-command"/> parameter or the
+    <xref linkend="guc-restore-library"/>'s archive cleanup callbacks to remove
+    files that are no longer required by the standby server.
     The <application>pg_archivecleanup</application> utility is designed specifically to
     be used with <varname>archive_cleanup_command</varname> in typical single-standby
     configurations, see <xref linkend="pgarchivecleanup"/>.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 8e1e71c256..f06385bea5 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -84,7 +84,7 @@
 #include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
-#include "restore/shell_restore.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
@@ -4891,18 +4891,19 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during log
-	 * shipping replication.  All files earlier than this point can be deleted
-	 * from the archive, though there is no requirement to do so.
+	 * The callback is provided the archive file cutoff point for use during
+	 * log shipping replication.  All files earlier than this point can be
+	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_recovery_end_configured())
+	if (recovery_end_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_recovery_end(lastRestartPointFname);
+		RestoreCallbacks->recovery_end_cb(restore_module_state,
+										  lastRestartPointFname);
 	}
 
 	/*
@@ -7317,18 +7318,19 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Finally, execute archive-cleanup callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during log
-	 * shipping replication.  All files earlier than this point can be deleted
-	 * from the archive, though there is no requirement to do so.
+	 * The callback is provided the archive file cutoff point for use during
+	 * log shipping replication.  All files earlier than this point can be
+	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_archive_cleanup_configured())
+	if (archive_cleanup_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_archive_cleanup(lastRestartPointFname);
+		RestoreCallbacks->archive_cleanup_cb(restore_module_state,
+											 lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index 4b45ea8753..50bcda1120 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -23,15 +23,22 @@
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
 #include "common/archive.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/startup.h"
 #include "postmaster/pgarch.h"
 #include "replication/walsender.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/lwlock.h"
+#include "utils/memutils.h"
+
+char *restoreLibrary = "";
+const RestoreModuleCallbacks *RestoreCallbacks = NULL;
+RestoreModuleState *restore_module_state;
 
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
@@ -75,15 +82,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			if (!shell_restore_file_configured())
+			if (!restore_wal_segment_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			if (!shell_restore_file_configured())
+			if (!restore_timeline_history_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			if (!shell_restore_file_configured())
+			if (!timeline_history_exists_configured())
 				goto not_available;
 			break;
 	}
@@ -175,14 +182,17 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			ret = shell_restore_wal_segment(xlogfname, xlogpath,
-											lastRestartPointFname);
+			ret = RestoreCallbacks->restore_wal_segment_cb(restore_module_state,
+														   xlogfname, xlogpath,
+														   lastRestartPointFname);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->restore_timeline_history_cb(restore_module_state,
+																xlogfname, xlogpath);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->timeline_history_exists_cb(restore_module_state,
+															   xlogfname, xlogpath);
 			break;
 	}
 
@@ -190,6 +200,16 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 
 	if (ret)
 	{
+		/*
+		 * Some restore functions might not copy the file (e.g., checking
+		 * whether a timeline history file exists), but they can set a flag to
+		 * tell us if they do.  We only need to verify file existence if this
+		 * flag is enabled.
+		 */
+		if (archive_type == ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS &&
+			!restore_module_state->timeline_history_exists_cb_copies)
+			return true;
+
 		/*
 		 * command apparently succeeded, but let's make sure the file is
 		 * really there now and has the correct size.
@@ -631,3 +651,65 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the restore callbacks into our global RestoreCallbacks.  The
+ * caller is responsible for validating the combination of library/command
+ * parameters that are set (e.g., restore_command and restore_library cannot
+ * both be set).
+ */
+void
+LoadRestoreCallbacks(void)
+{
+	RestoreModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_restore_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RestoreModuleInit)
+				load_external_function(restoreLibrary, "_PG_restore_module_init",
+									   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("restore modules have to define the symbol "
+						"_PG_restore_module_init")));
+
+	RestoreCallbacks = (*init) ();
+
+	/* restore state should be freed before calling this function */
+	Assert(restore_module_state == NULL);
+	restore_module_state = (RestoreModuleState *)
+							MemoryContextAllocZero(TopMemoryContext,
+												   sizeof(RestoreModuleState));
+
+	if (RestoreCallbacks->startup_cb != NULL)
+		RestoreCallbacks->startup_cb(restore_module_state);
+}
+
+/*
+ * Call the shutdown callback of the loaded restore module, if defined.  Also,
+ * free the restore module state if it was allocated.
+ *
+ * Processes that load restore libraries should register this via
+ * before_shmem_exit().
+ */
+void
+call_restore_module_shutdown_cb(int code, Datum arg)
+{
+	if (RestoreCallbacks != NULL &&
+		RestoreCallbacks->shutdown_cb != NULL &&
+		restore_module_state != NULL)
+		RestoreCallbacks->shutdown_cb(restore_module_state);
+
+	if (restore_module_state != NULL)
+	{
+		pfree(restore_module_state);
+		restore_module_state = NULL;
+	}
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index f0e1007f92..c6645c2413 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -50,6 +50,7 @@
 #include "postmaster/startup.h"
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
+#include "restore/restore_module.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1078,18 +1079,21 @@ validateRecoveryParameters(void)
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			(!restore_wal_segment_configured() ||
+			 !restore_timeline_history_configured() ||
+			 !timeline_history_exists_configured()))
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
+					(errmsg("specified neither primary_conninfo nor restore_command nor a configured restore_library"),
 					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
 	}
 	else
 	{
-		if (recoveryRestoreCommand == NULL ||
-			strcmp(recoveryRestoreCommand, "") == 0)
+		if (!restore_wal_segment_configured() ||
+			!restore_timeline_history_configured() ||
+			!timeline_history_exists_configured())
 			ereport(FATAL,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("must specify restore_command when standby mode is not enabled")));
+					 errmsg("must specify restore_command or a configured restore_library when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index aaad5c5228..73ff592f9f 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -45,6 +45,7 @@
 #include "postmaster/bgwriter.h"
 #include "postmaster/interrupt.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -233,6 +234,24 @@ CheckpointerMain(void)
 												 ALLOCSET_DEFAULT_SIZES);
 	MemoryContextSwitchTo(checkpointer_context);
 
+	/*
+	 * Load restore_library so that we can use its archive-cleanup callback, if
+	 * one is defined.
+	 *
+	 * We also take this opportunity to set up the before_shmem_exit hook for
+	 * the shutdown callback and to check that only one of restore_library,
+	 * archive_cleanup_command is set.  Note that we emit a WARNING upon
+	 * detecting misconfigured parameters, and we clear the callbacks so that
+	 * the archive-cleanup functionality is skipped.  We judge this
+	 * functionality to not be important enough to require blocking checkpoints
+	 * or shutting down the server when the parameters are misconfigured.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+										 archiveCleanupCommand, "archive_cleanup_command",
+										 WARNING))
+		LoadRestoreCallbacks();
+
 	/*
 	 * If an exception is encountered, processing resumes here.
 	 *
@@ -548,6 +567,10 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		bool		archive_cleanup_settings_changed = false;
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -563,6 +586,36 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		/*
+		 * If the archive-cleanup settings have changed, call the currently
+		 * loaded shutdown callback and clear all the restore callbacks.
+		 * There's presently no good way to unload a library besides restarting
+		 * the process, and there should be little harm in leaving it around,
+		 * so we just leave it loaded.
+		 */
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			archive_cleanup_settings_changed = true;
+			call_restore_module_shutdown_cb(0, (Datum) 0);
+			RestoreCallbacks = NULL;
+		}
+
+		/*
+		 * As in CheckpointerMain(), we only emit a WARNING if we detect that
+		 * both restore_library and archive_cleanup_command are set.  We do
+		 * this even if the archive-cleanup settings haven't changed to remind
+		 * the user about the misconfiguration.
+		 */
+		if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											 archiveCleanupCommand, "archive_cleanup_command",
+											 WARNING) &&
+			archive_cleanup_settings_changed)
+			LoadRestoreCallbacks();
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index adce0ffcef..a096b79182 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -29,6 +29,7 @@
 #include "pgstat.h"
 #include "postmaster/interrupt.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
 #include "storage/pmsignal.h"
@@ -148,13 +149,17 @@ StartupProcShutdownHandler(SIGNAL_ARGS)
  * Re-read the config file.
  *
  * If one of the critical walreceiver options has changed, flag xlog.c
- * to restart it.
+ * to restart it.  Also, check for invalid combinations of the command/library
+ * parameters and reload the restore callbacks if necessary.
  */
 static void
 StartupRereadConfig(void)
 {
 	char	   *conninfo = pstrdup(PrimaryConnInfo);
 	char	   *slotname = pstrdup(PrimarySlotName);
+	char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+	char	   *prevRestoreCommand = pstrdup(recoveryRestoreCommand);
+	char	   *prevRecoveryEndCommand = pstrdup(recoveryEndCommand);
 	bool		tempSlot = wal_receiver_create_temp_slot;
 	bool		conninfoChanged;
 	bool		slotnameChanged;
@@ -176,6 +181,30 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	/*
+	 * If the restore settings have changed, call the currently loaded shutdown
+	 * callback and load the new callbacks.  There's presently no good way to
+	 * unload a library besides restarting the process, and there should be
+	 * little harm in leaving it around, so we just leave it loaded.
+	 */
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_restore_module_shutdown_cb(0, (Datum) 0);
+		LoadRestoreCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
@@ -285,6 +314,19 @@ StartupProcessMain(void)
 	 */
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	LoadRestoreCallbacks();
+
 	/*
 	 * Do what we came for.
 	 */
diff --git a/src/backend/restore/shell_restore.c b/src/backend/restore/shell_restore.c
index fcd5475c88..b67cd4ac48 100644
--- a/src/backend/restore/shell_restore.c
+++ b/src/backend/restore/shell_restore.c
@@ -3,7 +3,8 @@
  * shell_restore.c
  *
  * These restore functions use a user-specified shell command (e.g., the
- * restore_command GUC).
+ * restore_command GUC).  It is used as the default, but other modules may
+ * define their own restore logic.
  *
  * Copyright (c) 2023, PostgreSQL Global Development Group
  *
@@ -22,25 +23,76 @@
 #include "common/archive.h"
 #include "common/percentrepl.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static void shell_restore_startup(RestoreModuleState *state);
+static bool shell_restore_file_configured(RestoreModuleState *state);
 static bool shell_restore_file(const char *file, const char *path,
 							   const char *lastRestartPointFileName);
+static bool shell_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool shell_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool shell_archive_cleanup_configured(RestoreModuleState *state);
+static void shell_archive_cleanup(RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+static bool shell_recovery_end_configured(RestoreModuleState *state);
+static void shell_recovery_end(RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
 static void ExecuteRecoveryCommand(const char *command,
 								   const char *commandName,
 								   bool failOnSignal,
 								   uint32 wait_event_info,
 								   const char *lastRestartPointFileName);
 
+static const RestoreModuleCallbacks shell_restore_callbacks = {
+	.startup_cb = shell_restore_startup,
+	.restore_wal_segment_configured_cb = shell_restore_file_configured,
+	.restore_wal_segment_cb = shell_restore_wal_segment,
+	.restore_timeline_history_configured_cb = shell_restore_file_configured,
+	.restore_timeline_history_cb = shell_restore_timeline_history,
+	.timeline_history_exists_configured_cb = shell_restore_file_configured,
+	.timeline_history_exists_cb = shell_restore_timeline_history,
+	.archive_cleanup_configured_cb = shell_archive_cleanup_configured,
+	.archive_cleanup_cb = shell_archive_cleanup,
+	.recovery_end_configured_cb = shell_recovery_end_configured,
+	.recovery_end_cb = shell_recovery_end,
+	.shutdown_cb = NULL
+};
+
+/*
+ * shell_restore_init
+ *
+ * Returns the callbacks for restoring via shell.
+ */
+const RestoreModuleCallbacks *
+shell_restore_init(void)
+{
+	return &shell_restore_callbacks;
+}
+
+/*
+ * Indicates that our timeline_history_exists_cb only attempts to retrieve the
+ * file, so the caller should verify the file exists afterwards.
+ */
+static void
+shell_restore_startup(RestoreModuleState *state)
+{
+	state->timeline_history_exists_cb_copies = true;
+}
+
 /*
  * Attempt to execute a shell-based restore command to retrieve a WAL segment.
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_wal_segment(const char *file, const char *path,
+static bool
+shell_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
 						  const char *lastRestartPointFileName)
 {
 	return shell_restore_file(file, path, lastRestartPointFileName);
@@ -52,8 +104,9 @@ shell_restore_wal_segment(const char *file, const char *path,
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_timeline_history(const char *file, const char *path)
+static bool
+shell_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
 {
 	char		lastRestartPointFname[MAXPGPATH];
 
@@ -64,8 +117,8 @@ shell_restore_timeline_history(const char *file, const char *path)
 /*
  * Check whether restore_command is supplied.
  */
-bool
-shell_restore_file_configured(void)
+static bool
+shell_restore_file_configured(RestoreModuleState *state)
 {
 	return recoveryRestoreCommand[0] != '\0';
 }
@@ -153,8 +206,8 @@ shell_restore_file(const char *file, const char *path,
 /*
  * Check whether archive_cleanup_command is supplied.
  */
-bool
-shell_archive_cleanup_configured(void)
+static bool
+shell_archive_cleanup_configured(RestoreModuleState *state)
 {
 	return archiveCleanupCommand[0] != '\0';
 }
@@ -162,8 +215,9 @@ shell_archive_cleanup_configured(void)
 /*
  * Attempt to execute a shell-based archive cleanup command.
  */
-void
-shell_archive_cleanup(const char *lastRestartPointFileName)
+static void
+shell_archive_cleanup(RestoreModuleState *state,
+					  const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(archiveCleanupCommand, "archive_cleanup_command",
 						   false, WAIT_EVENT_ARCHIVE_CLEANUP_COMMAND,
@@ -173,8 +227,8 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 /*
  * Check whether recovery_end_command is supplied.
  */
-bool
-shell_recovery_end_configured(void)
+static bool
+shell_recovery_end_configured(RestoreModuleState *state)
 {
 	return recoveryEndCommand[0] != '\0';
 }
@@ -182,8 +236,9 @@ shell_recovery_end_configured(void)
 /*
  * Attempt to execute a shell-based end-of-recovery command.
  */
-void
-shell_recovery_end(const char *lastRestartPointFileName)
+static void
+shell_recovery_end(RestoreModuleState *state,
+				   const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(recoveryEndCommand, "recovery_end_command", true,
 						   WAIT_EVENT_RECOVERY_END_COMMAND,
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1c0583fe26..a42819399b 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -63,6 +63,7 @@
 #include "replication/logicallauncher.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -3788,6 +3789,16 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"restore_library", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
+			gettext_noop("Sets the library that will be called for restore actions."),
+			NULL
+		},
+		&restoreLibrary,
+		"",
+		NULL, NULL, NULL
+	},
+
 	{
 		{"archive_cleanup_command", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
 			gettext_noop("Sets the shell command that will be executed at every restart point."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index d06074b86f..53757fea5f 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -270,6 +270,7 @@
 				# placeholders: %p = path of file to restore
 				#               %f = file name only
 				# e.g. 'cp /mnt/server/archivedir/%f %p'
+#restore_library = ''		# library to use for restore actions
 #archive_cleanup_command = ''	# command to execute at every restartpoint
 #recovery_end_command = ''	# command to execute at completion of recovery
 
diff --git a/src/include/restore/restore_module.h b/src/include/restore/restore_module.h
new file mode 100644
index 0000000000..c0aef33ba5
--- /dev/null
+++ b/src/include/restore/restore_module.h
@@ -0,0 +1,142 @@
+/*-------------------------------------------------------------------------
+ *
+ * restore_module.h
+ *		Exports for restore modules.
+ *
+ * Copyright (c) 2023, PostgreSQL Global Development Group
+ *
+ * src/include/restore/restore_module.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef _RESTORE_MODULE_H
+#define _RESTORE_MODULE_H
+
+/*
+ * The value of the restore_library GUC.
+ */
+extern PGDLLIMPORT char *restoreLibrary;
+
+typedef struct RestoreModuleState
+{
+	/*
+	 * Private data pointer for use by a restore module.  This can be used to
+	 * store state for the module that will be passed to each of its callbacks.
+	 */
+	void	   *private_data;
+
+	/*
+	 * Indicates whether the callback that checks for timeline existence merely
+	 * copies the file.  If set to true, the server will verify that the
+	 * timeline history file exists after the callback returns.
+	 */
+	bool		timeline_history_exists_cb_copies;
+} RestoreModuleState;
+
+/*
+ * Restore module callbacks
+ *
+ * These callback functions should be defined by restore libraries and returned
+ * via _PG_restore_module_init().  For more information about the purpose of
+ * each callback, refer to the restore modules documentation.
+ */
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state,
+									 const char *file, const char *path,
+									 const char *lastRestartPointFileName);
+typedef bool (*RestoreTimelineHistoryConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state,
+										  const char *file, const char *path);
+typedef bool (*TimelineHistoryExistsConfiguredCB) (RestoreModuleState *state);
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state,
+										 const char *file, const char *path);
+typedef bool (*ArchiveCleanupConfiguredCB) (RestoreModuleState *state);
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+typedef bool (*RecoveryEndConfiguredCB) (RestoreModuleState *state);
+typedef void (*RecoveryEndCB) (RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+
+typedef struct RestoreModuleCallbacks
+{
+	RestoreStartupCB startup_cb;
+	RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+	RestoreWalSegmentCB restore_wal_segment_cb;
+	RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+	RestoreTimelineHistoryCB restore_timeline_history_cb;
+	TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+	TimelineHistoryExistsCB timeline_history_exists_cb;
+	ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+	ArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndConfiguredCB recovery_end_configured_cb;
+	RecoveryEndCB recovery_end_cb;
+	RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+
+/*
+ * Type of the shared library symbol _PG_restore_module_init that is looked up
+ * when loading a restore library.
+ */
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+
+extern PGDLLEXPORT const RestoreModuleCallbacks *_PG_restore_module_init(void);
+
+extern void LoadRestoreCallbacks(void);
+extern void call_restore_module_shutdown_cb(int code, Datum arg);
+
+extern const RestoreModuleCallbacks *RestoreCallbacks;
+extern RestoreModuleState *restore_module_state;
+
+/*
+ * The following *_configured() functions are convenient wrappers around the
+ * *_configured_cb() callback functions with additional defensive NULL checks.
+ */
+
+static inline bool
+restore_wal_segment_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		   RestoreCallbacks->restore_wal_segment_configured_cb != NULL &&
+		   RestoreCallbacks->restore_wal_segment_cb != NULL &&
+		   RestoreCallbacks->restore_wal_segment_configured_cb(restore_module_state);
+}
+
+static inline bool
+restore_timeline_history_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		   RestoreCallbacks->restore_timeline_history_configured_cb != NULL &&
+		   RestoreCallbacks->restore_timeline_history_cb != NULL &&
+		   RestoreCallbacks->restore_timeline_history_configured_cb(restore_module_state);
+}
+
+static inline bool
+timeline_history_exists_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		   RestoreCallbacks->timeline_history_exists_configured_cb != NULL &&
+		   RestoreCallbacks->timeline_history_exists_cb != NULL &&
+		   RestoreCallbacks->timeline_history_exists_configured_cb(restore_module_state);
+}
+
+static inline bool
+archive_cleanup_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		   RestoreCallbacks->archive_cleanup_configured_cb != NULL &&
+		   RestoreCallbacks->archive_cleanup_cb != NULL &&
+		   RestoreCallbacks->archive_cleanup_configured_cb(restore_module_state);
+}
+
+static inline bool
+recovery_end_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		   RestoreCallbacks->recovery_end_configured_cb != NULL &&
+		   RestoreCallbacks->recovery_end_cb != NULL &&
+		   RestoreCallbacks->recovery_end_configured_cb(restore_module_state);
+}
+
+#endif							/* _RESTORE_MODULE_H */
diff --git a/src/include/restore/shell_restore.h b/src/include/restore/shell_restore.h
index 570588e493..1eeae51d81 100644
--- a/src/include/restore/shell_restore.h
+++ b/src/include/restore/shell_restore.h
@@ -12,15 +12,13 @@
 #ifndef _SHELL_RESTORE_H
 #define _SHELL_RESTORE_H
 
-extern bool shell_restore_file_configured(void);
-extern bool shell_restore_wal_segment(const char *file, const char *path,
-									  const char *lastRestartPointFileName);
-extern bool shell_restore_timeline_history(const char *file, const char *path);
+#include "restore/restore_module.h"
 
-extern bool shell_archive_cleanup_configured(void);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-
-extern bool shell_recovery_end_configured(void);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Since the logic for restoring via shell commands is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern const RestoreModuleCallbacks *shell_restore_init(void);
 
 #endif							/* _SHELL_RESTORE_H */
-- 
2.25.1


--5vNYLRcllDrimb99--





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

* [PATCH v14 7/7] introduce restore_library
@ 2023-02-16 06:06  Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 28+ messages in thread

From: Nathan Bossart @ 2023-02-16 06:06 UTC (permalink / raw)

---
 contrib/basic_archive/Makefile                |   1 +
 contrib/basic_archive/basic_archive.c         | 153 +++++++-
 contrib/basic_archive/meson.build             |   5 +
 contrib/basic_archive/t/001_restore.pl        |  44 +++
 doc/src/sgml/archive-and-restore-modules.sgml | 356 +++++++++++++++++-
 doc/src/sgml/backup.sgml                      |  40 +-
 doc/src/sgml/basic-archive.sgml               |  31 +-
 doc/src/sgml/config.sgml                      |  53 ++-
 doc/src/sgml/high-availability.sgml           |  18 +-
 src/backend/access/transam/xlog.c             |  28 +-
 src/backend/access/transam/xlogarchive.c      |  96 ++++-
 src/backend/access/transam/xlogrecovery.c     |  14 +-
 src/backend/postmaster/checkpointer.c         |  53 +++
 src/backend/postmaster/startup.c              |  44 ++-
 src/backend/restore/shell_restore.c           |  85 ++++-
 src/backend/utils/misc/guc_tables.c           |  11 +
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/restore/restore_module.h          | 142 +++++++
 src/include/restore/shell_restore.h           |  16 +-
 19 files changed, 1103 insertions(+), 88 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl
 create mode 100644 src/include/restore/restore_module.h

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 55d299d650..d12d45e0d2 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -5,6 +5,7 @@ PGFILEDESC = "basic_archive - basic archive module"
 
 REGRESS = basic_archive
 REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.conf
+TAP_TESTS = 1
 # Disabled because these tests require "shared_preload_libraries=basic_archive",
 # which typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index cd852888ce..b04d83da4c 100644
--- a/contrib/basic_archive/basic_archive.c
+++ b/contrib/basic_archive/basic_archive.c
@@ -17,6 +17,11 @@
  * a file is successfully archived and then the system crashes before
  * a durable record of the success has been made.
  *
+ * This file also demonstrates a basic restore library implementation that
+ * is roughly equivalent to the following shell command:
+ *
+ *		cp /path/to/archivedir/%f %p
+ *
  * Copyright (c) 2022-2023, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
@@ -33,6 +38,7 @@
 #include "archive/archive_module.h"
 #include "common/int.h"
 #include "miscadmin.h"
+#include "restore/restore_module.h"
 #include "storage/copydir.h"
 #include "storage/fd.h"
 #include "utils/guc.h"
@@ -54,6 +60,16 @@ static void basic_archive_file_internal(const char *file, const char *path);
 static bool check_archive_directory(char **newval, void **extra, GucSource source);
 static bool compare_files(const char *file1, const char *file2);
 static void basic_archive_shutdown(ArchiveModuleState *state);
+static bool basic_restore_configured(RestoreModuleState *state);
+static bool basic_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool basic_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool basic_restore_file(const char *file, const char *path);
+static bool basic_restore_timeline_history_exists(RestoreModuleState *state,
+												  const char *file,
+												  const char *path);
 
 static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.startup_cb = basic_archive_startup,
@@ -62,6 +78,21 @@ static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.shutdown_cb = basic_archive_shutdown
 };
 
+static const RestoreModuleCallbacks basic_restore_callbacks = {
+	.startup_cb = NULL,
+	.restore_wal_segment_configured_cb = basic_restore_configured,
+	.restore_wal_segment_cb = basic_restore_wal_segment,
+	.restore_timeline_history_configured_cb = basic_restore_configured,
+	.restore_timeline_history_cb = basic_restore_timeline_history,
+	.timeline_history_exists_configured_cb = basic_restore_configured,
+	.timeline_history_exists_cb = basic_restore_timeline_history_exists,
+	.archive_cleanup_configured_cb = NULL,
+	.archive_cleanup_cb = NULL,
+	.recovery_end_configured_cb = NULL,
+	.recovery_end_cb = NULL,
+	.shutdown_cb = NULL
+};
+
 /*
  * _PG_init
  *
@@ -71,7 +102,7 @@ void
 _PG_init(void)
 {
 	DefineCustomStringVariable("basic_archive.archive_directory",
-							   gettext_noop("Archive file destination directory."),
+							   gettext_noop("Archive file source/destination directory."),
 							   NULL,
 							   &archive_directory,
 							   "",
@@ -93,6 +124,17 @@ _PG_archive_module_init(void)
 	return &basic_archive_callbacks;
 }
 
+/*
+ * _PG_restore_module_init
+ *
+ * Returns the module's restore callbacks.
+ */
+const RestoreModuleCallbacks *
+_PG_restore_module_init(void)
+{
+	return &basic_restore_callbacks;
+}
+
 /*
  * basic_archive_startup
  *
@@ -426,3 +468,112 @@ basic_archive_shutdown(ArchiveModuleState *state)
 	pfree(data);
 	state->private_data = NULL;
 }
+
+/*
+ * basic_restore_configured
+ *
+ * Checks that archive_directory is not blank.
+ */
+static bool
+basic_restore_configured(RestoreModuleState *state)
+{
+	return archive_directory != NULL && archive_directory[0] != '\0';
+}
+
+/*
+ * basic_restore_wal_segment
+ *
+ * Retrieves one archived WAL segment.
+ */
+static bool
+basic_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
+						  const char *lastRestartPointFileName)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_timeline_history
+ *
+ * Retrieves one timeline history file.
+ */
+static bool
+basic_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file.
+ */
+static bool
+basic_restore_file(const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive",
+					file)));
+
+	/*
+	 * If the file doesn't exist, return false to indicate that there are no
+	 * more files to restore.
+	 */
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	copy_file(archive_path, path);
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive",
+					file)));
+	return true;
+}
+
+/*
+ * basic_restore_timeline_history_exists
+ *
+ * Check whether a timeline history file is present in the archive directory.
+ */
+static bool
+basic_restore_timeline_history_exists(RestoreModuleState *state,
+									  const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("checking existence of \"%s\" via basic_archive",
+					file)));
+
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	ereport(DEBUG1,
+			(errmsg("verified existence of \"%s\" via basic_archive",
+					file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index bc1380e6f6..4e9f5002de 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -31,4 +31,9 @@ tests += {
     # which typical runningcheck users do not have (e.g. buildfarm clients).
     'runningcheck': false,
   },
+  'tap': {
+    'tests': [
+      't/001_restore.pl',
+    ],
+  },
 }
diff --git a/contrib/basic_archive/t/001_restore.pl b/contrib/basic_archive/t/001_restore.pl
new file mode 100644
index 0000000000..f7fa124695
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,44 @@
+
+# Copyright (c) 2023, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# start a node
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init(has_archiving => 1, allows_streaming => 1);
+my $archive_dir = $node->archive_dir;
+$archive_dir =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
+$node->append_conf('postgresql.conf', "archive_command = ''");
+$node->append_conf('postgresql.conf', "archive_library = 'basic_archive'");
+$node->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$node->start;
+
+# backup the node
+my $backup = 'backup';
+$node->backup($backup);
+
+# generate some new WAL files
+$node->safe_psql('postgres', "CREATE TABLE test (a INT);");
+$node->safe_psql('postgres', "SELECT pg_switch_wal();");
+$node->safe_psql('postgres', "INSERT INTO test VALUES (1);");
+
+# shut down the node (this should archive all WAL files)
+$node->stop;
+
+# restore from the backup
+my $restore = PostgreSQL::Test::Cluster->new('restore');
+$restore->init_from_backup($node, $backup, has_restoring => 1, standby => 0);
+$restore->append_conf('postgresql.conf', "restore_command = ''");
+$restore->append_conf('postgresql.conf', "restore_library = 'basic_archive'");
+$restore->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$restore->start;
+
+# ensure post-backup WAL is replayed
+my $result = $restore->poll_query_until("postgres", "SELECT count(*) = 1 FROM test;");
+is($result, "1", "check restore content");
+
+done_testing();
diff --git a/doc/src/sgml/archive-and-restore-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml
index f30ee591e7..56373a211b 100644
--- a/doc/src/sgml/archive-and-restore-modules.sgml
+++ b/doc/src/sgml/archive-and-restore-modules.sgml
@@ -8,15 +8,17 @@
 
  <para>
   PostgreSQL provides infrastructure to create custom modules for continuous
-  archiving (see <xref linkend="continuous-archiving"/>).  While archiving via
-  a shell command (i.e., <xref linkend="guc-archive-command"/>) is much
-  simpler, a custom archive module will often be considerably more robust and
-  performant.
+  archiving and recovery (see <xref linkend="continuous-archiving"/>).  While a
+  shell command (i.e., <xref linkend="guc-archive-command"/>,
+  <xref linkend="guc-restore-command"/>) is much simpler, a custom module will
+  often be considerably more robust and performant.
  </para>
 
  <para>
   The <filename>contrib/basic_archive</filename> module contains a working
-  example, which demonstrates some useful techniques.
+  example, which demonstrates some useful techniques.  As demonstrated in
+  <filename>basic_archive</filename> a single module may serve as both an
+  archive module and a restore module.
  </para>
 
  <sect1 id="archive-modules">
@@ -167,4 +169,348 @@ typedef void (*ArchiveShutdownCB) (ArchiveModuleState *state);
    </sect3>
   </sect2>
  </sect1>
+
+ <sect1 id="restore-modules">
+  <title>Restore Modules</title>
+  <indexterm zone="restore-modules">
+   <primary>Restore Modules</primary>
+  </indexterm>
+
+  <para>
+   When a custom <xref linkend="guc-restore-library"/> is configured,
+   PostgreSQL will use the module for restore actions.  It is
+   ultimately up to the module to decide how to accomplish each task,
+   but some recommendations are listed at
+   <xref linkend="backup-pitr-recovery"/>.
+  </para>
+
+  <para>
+   Restore modules must at least consist of an initialization function
+   (see <xref linkend="restore-module-init"/>) and the required callbacks (see
+   <xref linkend="restore-module-callbacks"/>).  However, restore modules are
+   also permitted to do much more (e.g., declare GUCs and register background
+   workers).
+  </para>
+
+  <sect2 id="restore-module-init">
+   <title>Initialization Functions</title>
+   <indexterm zone="restore-module-init">
+    <primary>_PG_restore_module_init</primary>
+   </indexterm>
+
+   <para>
+    An restore library is loaded by dynamically loading a shared library with
+    the <xref linkend="guc-restore-library"/>'s name as the library base name.
+    The normal library search path is used to locate the library.  To provide
+    the required restore module callbacks and to indicate that the library is
+    actually a restore module, it needs to provide a function named
+    <function>_PG_restore_module_init</function>.  The result of the function
+    must be a pointer to a struct of type
+    <structname>RestoreModuleCallbacks</structname>, which contains everything
+    that the core code needs to know how to make use of the restore module.
+    The return value needs to be of server lifetime, which is typically
+    achieved by defining it as a <literal>static const</literal> variable in
+    global scope.
+
+<programlisting>
+typedef struct RestoreModuleCallbacks
+{
+    RestoreStartupCB startup_cb;
+    RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+    RestoreWalSegmentCB restore_wal_segment_cb;
+    RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+    RestoreTimelineHistoryCB restore_timeline_history_cb;
+    TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+    TimelineHistoryExistsCB timeline_history_exists_cb;
+    ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+    ArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndConfiguredCB recovery_end_configured_cb;
+    RecoveryEndCB recovery_end_cb;
+    RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+</programlisting>
+
+    The <function>restore_wal_segment_configured_cb</function>,
+    <function>restore_wal_segment_cb</function>,
+    <function>restore_timeline_history_configured_cb</function>,
+    <function>restore_timeline_history_cb</function>,
+    <function>timeline_history_exists_configured_cb</function>, and
+    <function>timeline_history_exists_cb</function> callbacks are required for
+    archive recovery but optional for streaming replication.  The others are
+    always optional.
+   </para>
+  </sect2>
+
+  <sect2 id="restore-module-callbacks">
+   <title>Restore Module Callbacks</title>
+
+   <para>
+    The restore callbacks define the actual behavior of the module.  The server
+    will call them as required to execute restore actions.
+   </para>
+
+   <sect3 id="restore-module-startup">
+    <title>Startup Callback</title>
+
+    <para>
+     The <function>startup_cb</function> callback is called shortly after the
+     module is loaded.  This callback can be used to perform any additional
+     initialization required.  If the restore module has state, it can use
+     <structfield>state->private_data</structfield> to store it.
+
+<programlisting>
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+</programlisting>
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment-configured">
+    <title>Restore WAL Segment Configured Callback</title>
+    <para>
+     The <function>restore_wal_segment_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to retrieve WAL files (e.g., its configuration parameters
+     are set to valid values).  If no
+     <function>restore_wal_segment_configured_cb</function> is defined, the
+     server always assumes the module is not configured to retrieve WAL files.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve WAL files
+     by calling the <function>restore_wal_segment_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_wal_segment_cb</function> callback to retrieve WAL
+     files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment">
+    <title>Restore WAL Segment Callback</title>
+    <para>
+     The <function>restore_wal_segment_cb</function> callback is called to
+     retrieve a single archived segment of the WAL file series for archive
+     recovery or streaming replication.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state, const char *file, const char *path, const char *lastRestartPointFileName);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point.  That is the earliest
+     file that must be kept to allow a restore to be restartable, so this
+     information can be used to truncate the archive to just the minimum
+     required to support restarting from the current restore.
+     <replaceable>lastRestartPointFileName</replaceable> is typically only used
+     by warm-standby configurations (see <xref linkend="warm-standby"/>).  Note
+     that if multiple standby servers are restoring from the same archive
+     directory, you will need to ensure that you do not delete WAL files until
+     they are no longer needed by any of the servers.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history-configured">
+    <title>Restore Timeline History Configured Callback</title>
+    <para>
+     The <function>restore_timeline_history_configured_cb</function> callback
+     is called to determine whether the module is fully configured and ready to
+     accept requests to retrieve timeline history files (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>restore_timeline_history_configured_cb</function> is defined,
+     the server always assumes the module is not configured to retrieve
+     timeline history files.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve timeline
+     history files by calling the
+     <function>restore_timeline_history_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_timeline_history_cb</function> callback to retrieve
+     timeline history files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history">
+    <title>Restore Timeline History Callback</title>
+    <para>
+     The <function>restore_timeline_history_cb</function> callback is called to
+     retrieve a single archived timeline history file for archive recovery or
+     streaming replication.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists-configured">
+    <title>Timeline History Exists Configured Callback</title>
+    <para>
+     The <function>timeline_history_exists_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to determine whether a timeline history file exists in the
+     archives (e.g., its configuration parameters are set to valid values).  If
+     no <function>timeline_history_exists_configured_cb</function> is defined,
+     the server always assumes the module is not configured to check whether
+     certain timeline history files exist.
+
+<programlisting>
+typedef bool (*TimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will check whether
+     certain timeline history files are present in the archives by calling the
+     <function>timeline_history_exists_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>timeline_history_exists_cb</function> callback to check if
+     timeline history files exist in the archives.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists">
+    <title>Timeline History Exists Callback</title>
+    <para>
+     The <function>timeline_history_exists_cb</function> callback is called to
+     check whether a timeline history file is present in the archives.
+
+<programlisting>
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file is
+     present in the archives.  If the file is not available in the archives,
+     the callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the timeline history file.
+    </para>
+
+    <para>
+     Some restore modules might not have a dedicated way to determine whether a
+     timeline history file exists.  For example,
+     <varname>restore_command</varname> only tells the server how to retrieve
+     files.  In this case, the <function>timeline_history_exists_cb</function>
+     callback should copy the file to <replaceable>path</replaceable>, which
+     contains the destination's relative path (including the file name), and
+     the restore module should set
+     <structfield>state->timeline_history_exists_cb_copies</structfield> to
+     <literal>true</literal>.  It is recommended to set this flag in the
+     module's <function>startup_cb</function> callback.  This flag tells the
+     server that it should verify that the file was successfully retrieved
+     after invoking this callback.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup-configured">
+    <title>Archive Cleanup Configured Callback</title>
+    <para>
+     The <function>archive_cleanup_configured_cb</function> callback is called
+     to determine whether the module is fully configured and ready to accept
+     requests to remove old WAL files from the archives (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>archive_cleanup_configured_cb</function> is defined, the server
+     always assumes the module is not configured to remove old WAL files.
+
+<programlisting>
+typedef bool (*ArchiveCleanupConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will remove old WAL
+     files by calling the <function>archive_cleanup_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>archive_cleanup_cb</function> callback to remove old WAL files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup">
+    <title>Archive Cleanup Callback</title>
+    <para>
+     The <function>archive_cleanup_cb</function> callback is called at every
+     restart point by the checkpointer process and is intended to provide a
+     mechanism for cleaning up old archived WAL files that are no longer
+     needed by the standby server.
+
+<programlisting>
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the
+     name of the file that includes the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end-configured">
+    <title>Recovery End Configured Callback</title>
+    <para>
+     The <function>recovery_end_configured_cb</function> callback is called to
+     determine whether the module is fully configured and ready to execute its
+     <function>recovery_end_cb</function> callback once at the end of recovery
+     (e.g., its configuration parameters are set to valid values).  If no
+     <function>recovery_end_configured_cb</function> is defined, the server
+     always assumes the module is not configured to execute its
+     <function>recovery_end_cb</function> callback.
+
+<programlisting>
+typedef bool (*RecoveryEndConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will execute the
+     <function>recovery_end_cb</function> callback once at the end of recovery.
+     If <literal>false</literal> is returned, the server will not use the
+     <function>recovery_end_cb</function> callback at the end of recovery.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end">
+    <title>Recovery End Callback</title>
+    <para>
+     The <function>recovery_end_cb</function> callback is called once at the
+     end of recovery and is intended to provide a mechanism for cleanup
+     following the end of replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-shutdown">
+    <title>Shutdown Callback</title>
+    <para>
+     The <function>shutdown_cb</function> callback is called when a process
+     that has loaded the restore module exits (e.g., after an error) or the
+     value of <xref linkend="guc-restore-library"/> changes.  If no
+     <function>shutdown_cb</function> is defined, no special action is taken in
+     these situations.  If the restore module has state, this callback should
+     free it to avoid leaks.
+
+<programlisting>
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+ </programlisting>
+    </para>
+   </sect3>
+  </sect2>
+ </sect1>
 </chapter>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index be05a33205..5555f23a85 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1180,9 +1180,26 @@ SELECT * FROM pg_backup_stop(wait_for_archive => true);
    <para>
     The key part of all this is to set up a recovery configuration that
     describes how you want to recover and how far the recovery should
-    run.  The one thing that you absolutely must specify is the <varname>restore_command</varname>,
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>,
     which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    WAL file segments.
+   </para>
+
+   <para>
+    Like the <varname>archive_library</varname> parameter,
+    <varname>restore_library</varname> is a shared library.  Since such
+    libraries are written in <literal>C</literal>, creating your own may
+    require considerably more effort than writing a shell command.  However,
+    restore modules can be more performance than restoring via shell, and they
+    will have access to many useful server resources.  For more information
+    about creating a <varname>restore_library</varname>, see
+    <xref linkend="restore-modules"/>.
+   </para>
+
+   <para>
+    Like the <varname>archive_command</varname>,
+    <varname>restore_command</varname> is
     a shell command string.  It can contain <literal>%f</literal>, which is
     replaced by the name of the desired WAL file, and <literal>%p</literal>,
     which is replaced by the path name to copy the WAL file to.
@@ -1201,14 +1218,20 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
    </para>
 
    <para>
-    It is important that the command return nonzero exit status on failure.
-    The command <emphasis>will</emphasis> be called requesting files that are not
-    present in the archive; it must return nonzero when so asked.  This is not
-    an error condition.  An exception is that if the command was terminated by
+    It is important that the <varname>restore_command</varname> return nonzero
+    exit status on failure, or, if you are using a
+    <varname>restore_library</varname>, that the restore callbacks return
+    <literal>false</literal> on failure.  The command or library
+    <emphasis>will</emphasis> be called requesting files that are not
+    present in the archive; it must fail when so asked.  This is not
+    an error condition.  An exception is that if the
+    <varname>restore_command</varname> was terminated by
     a signal (other than <systemitem>SIGTERM</systemitem>, which is used as
     part of a database server shutdown) or an error by the shell (such as
     command not found), then recovery will abort and the server will not start
-    up.
+    up.  Likewise, if the restore callbacks provided by the
+    <varname>restore_library</varname> emit an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1232,7 +1255,8 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
     close as possible given the available WAL segments).  Therefore, a normal
     recovery will end with a <quote>file not found</quote> message, the exact text
     of the error message depending upon your choice of
-    <varname>restore_command</varname>.  You may also see an error message
+    <varname>restore_command</varname> or <varname>restore_library</varname>.
+    You may also see an error message
     at the start of recovery for a file named something like
     <filename>00000001.history</filename>.  This is also normal and does not
     indicate a problem in simple recovery situations; see
diff --git a/doc/src/sgml/basic-archive.sgml b/doc/src/sgml/basic-archive.sgml
index b4d43ced20..f3a5a502bd 100644
--- a/doc/src/sgml/basic-archive.sgml
+++ b/doc/src/sgml/basic-archive.sgml
@@ -8,17 +8,20 @@
  </indexterm>
 
  <para>
-  <filename>basic_archive</filename> is an example of an archive module.  This
-  module copies completed WAL segment files to the specified directory.  This
-  may not be especially useful, but it can serve as a starting point for
-  developing your own archive module.  For more information about archive
-  modules, see <xref linkend="archive-modules"/>.
+  <filename>basic_archive</filename> is an example of an archive and restore
+  module.  This module copies completed WAL segment files to or from the
+  specified directory.  This may not be especially useful, but it can serve as
+  a starting point for developing your own archive and restore modules.  For
+  more information about archive and restore modules, see\
+  <xref linkend="archive-and-restore-modules"/>.
  </para>
 
  <para>
-  In order to function, this module must be loaded via
+  For use as an archive module, this module must be loaded via
   <xref linkend="guc-archive-library"/>, and <xref linkend="guc-archive-mode"/>
-  must be enabled.
+  must be enabled.  For use as a restore module, this module must be loaded
+  via <xref linkend="guc-restore-library"/>, and recovery must be enabled (see
+  <xref linkend="runtime-config-wal-archive-recovery"/>).
  </para>
 
  <sect2 id="basic-archive-configuration-parameters">
@@ -34,11 +37,12 @@
     </term>
     <listitem>
      <para>
-      The directory where the server should copy WAL segment files.  This
-      directory must already exist.  The default is an empty string, which
-      effectively halts WAL archiving, but if <xref linkend="guc-archive-mode"/>
-      is enabled, the server will accumulate WAL segment files in the
-      expectation that a value will soon be provided.
+      The directory where the server should copy WAL segment files to or from.
+      This directory must already exist.  The default is an empty string,
+      which, when used for archiving, effectively halts WAL archival, but if
+      <xref linkend="guc-archive-mode"/> is enabled, the server will accumulate
+      WAL segment files in the expectation that a value will soon be provided.
+      When an empty string is used for recovery, restore will fail.
      </para>
     </listitem>
    </varlistentry>
@@ -46,7 +50,7 @@
 
   <para>
    These parameters must be set in <filename>postgresql.conf</filename>.
-   Typical usage might be:
+   Typical usage as an archive module might be:
   </para>
 
 <programlisting>
@@ -61,6 +65,7 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
+   When <filename>basic_archive</filename> is used as an archive module,
    Server crashes may leave temporary files with the prefix
    <filename>archtemp</filename> in the archive directory.  It is recommended to
    delete such files before restarting the server after a crash.  It is safe to
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index e5c41cc6c6..288d58f48a 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3807,7 +3807,8 @@ include_dir 'conf.d'
      recovery when the end of archived WAL is reached, but will keep trying to
      continue recovery by connecting to the sending server as specified by the
      <varname>primary_conninfo</varname> setting and/or by fetching new WAL
-     segments using <varname>restore_command</varname>.  For this mode, the
+     segments using <varname>restore_command</varname> or
+     <varname>restore_library</varname>.  For this mode, the
      parameters from this section and <xref
      linkend="runtime-config-replication-standby"/> are of interest.
      Parameters from <xref linkend="runtime-config-wal-recovery-target"/> will
@@ -3835,7 +3836,8 @@ include_dir 'conf.d'
       <listitem>
        <para>
         The local shell command to execute to retrieve an archived segment of
-        the WAL file series. This parameter is required for archive recovery,
+        the WAL file series.  Either <varname>restore_command</varname> or
+        <xref linkend="guc-restore-library"/> is required for archive recovery,
         but optional for streaming replication.
         Any <literal>%f</literal> in the string is
         replaced by the name of the file to retrieve from the archive,
@@ -3870,7 +3872,42 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
 
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>restore_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-restore-library" xreflabel="restore_library">
+      <term><varname>restore_library</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>restore_library</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        The library to use for restore actions, including retrieving archived
+        files and executing tasks at restartpoints and at recovery end.  Either
+        <xref linkend="guc-restore-command"/> or
+        <varname>restore_library</varname> is required for archive recovery,
+        but optional for streaming replication.  If this parameter is set to an
+        empty string (the default), restoring via shell is enabled, and
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, and
+        <varname>recovery_end_command</varname> are used.  If both
+        <varname>restore_library</varname> and any of
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, or
+        <varname>recovery_end_command</varname> are set, an error will be
+        raised.  Otherwise, the specified shared library is used for restoring.
+        For more information, see <xref linkend="restore-modules"/>.
+       </para>
+
+       <para>
+        This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
        </para>
       </listitem>
      </varlistentry>
@@ -3915,7 +3952,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
        </para>
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>archive_cleanup_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
        </para>
       </listitem>
      </varlistentry>
@@ -3944,7 +3984,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
        </para>
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>recovery_end_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index 9d0deaeeb8..094140cecc 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -627,7 +627,8 @@ protocol to make nodes agree on a serializable transactional order.
    <para>
     In standby mode, the server continuously applies WAL received from the
     primary server. The standby server can read WAL from a WAL archive
-    (see <xref linkend="guc-restore-command"/>) or directly from the primary
+    (see <xref linkend="guc-restore-command"/> and
+    <xref linkend="guc-restore-library"/>) or directly from the primary
     over a TCP connection (streaming replication). The standby server will
     also attempt to restore any WAL found in the standby cluster's
     <filename>pg_wal</filename> directory. That typically happens after a server
@@ -638,8 +639,10 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     At startup, the standby begins by restoring all WAL available in the
-    archive location, calling <varname>restore_command</varname>. Once it
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s callbacks.  Once it
     reaches the end of WAL available there and <varname>restore_command</varname>
+    or <varname>restore_library</varname>
     fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
     If that fails, and streaming replication has been configured, the
     standby tries to connect to the primary server and start streaming WAL
@@ -698,7 +701,8 @@ protocol to make nodes agree on a serializable transactional order.
     server (see <xref linkend="backup-pitr-recovery"/>). Create a file
     <link linkend="file-standby-signal"><filename>standby.signal</filename></link><indexterm><primary>standby.signal</primary></indexterm>
     in the standby's cluster data
-    directory. Set <xref linkend="guc-restore-command"/> to a simple command to copy files from
+    directory. Set <xref linkend="guc-restore-command"/> or
+    <xref linkend="guc-restore-library"/> to copy files from
     the WAL archive. If you plan to have multiple standby servers for high
     availability purposes, make sure that <varname>recovery_target_timeline</varname> is set to
     <literal>latest</literal> (the default), to make the standby server follow the timeline change
@@ -707,7 +711,8 @@ protocol to make nodes agree on a serializable transactional order.
 
    <note>
      <para>
-     <xref linkend="guc-restore-command"/> should return immediately
+     <xref linkend="guc-restore-command"/> and
+     <xref linkend="guc-restore-library"/> should return immediately
      if the file does not exist; the server will retry the command again if
      necessary.
     </para>
@@ -731,8 +736,9 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     If you're using a WAL archive, its size can be minimized using the <xref
-    linkend="guc-archive-cleanup-command"/> parameter to remove files that are no
-    longer required by the standby server.
+    linkend="guc-archive-cleanup-command"/> parameter or the
+    <xref linkend="guc-restore-library"/>'s archive cleanup callbacks to remove
+    files that are no longer required by the standby server.
     The <application>pg_archivecleanup</application> utility is designed specifically to
     be used with <varname>archive_cleanup_command</varname> in typical single-standby
     configurations, see <xref linkend="pgarchivecleanup"/>.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index 8e1e71c256..f06385bea5 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -84,7 +84,7 @@
 #include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
-#include "restore/shell_restore.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
@@ -4891,18 +4891,19 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during log
-	 * shipping replication.  All files earlier than this point can be deleted
-	 * from the archive, though there is no requirement to do so.
+	 * The callback is provided the archive file cutoff point for use during
+	 * log shipping replication.  All files earlier than this point can be
+	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_recovery_end_configured())
+	if (recovery_end_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_recovery_end(lastRestartPointFname);
+		RestoreCallbacks->recovery_end_cb(restore_module_state,
+										  lastRestartPointFname);
 	}
 
 	/*
@@ -7317,18 +7318,19 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Finally, execute archive-cleanup callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during log
-	 * shipping replication.  All files earlier than this point can be deleted
-	 * from the archive, though there is no requirement to do so.
+	 * The callback is provided the archive file cutoff point for use during
+	 * log shipping replication.  All files earlier than this point can be
+	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_archive_cleanup_configured())
+	if (archive_cleanup_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_archive_cleanup(lastRestartPointFname);
+		RestoreCallbacks->archive_cleanup_cb(restore_module_state,
+											 lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index 4b45ea8753..50bcda1120 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -23,15 +23,22 @@
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
 #include "common/archive.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/startup.h"
 #include "postmaster/pgarch.h"
 #include "replication/walsender.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/lwlock.h"
+#include "utils/memutils.h"
+
+char *restoreLibrary = "";
+const RestoreModuleCallbacks *RestoreCallbacks = NULL;
+RestoreModuleState *restore_module_state;
 
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
@@ -75,15 +82,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			if (!shell_restore_file_configured())
+			if (!restore_wal_segment_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			if (!shell_restore_file_configured())
+			if (!restore_timeline_history_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			if (!shell_restore_file_configured())
+			if (!timeline_history_exists_configured())
 				goto not_available;
 			break;
 	}
@@ -175,14 +182,17 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			ret = shell_restore_wal_segment(xlogfname, xlogpath,
-											lastRestartPointFname);
+			ret = RestoreCallbacks->restore_wal_segment_cb(restore_module_state,
+														   xlogfname, xlogpath,
+														   lastRestartPointFname);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->restore_timeline_history_cb(restore_module_state,
+																xlogfname, xlogpath);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->timeline_history_exists_cb(restore_module_state,
+															   xlogfname, xlogpath);
 			break;
 	}
 
@@ -190,6 +200,16 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 
 	if (ret)
 	{
+		/*
+		 * Some restore functions might not copy the file (e.g., checking
+		 * whether a timeline history file exists), but they can set a flag to
+		 * tell us if they do.  We only need to verify file existence if this
+		 * flag is enabled.
+		 */
+		if (archive_type == ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS &&
+			!restore_module_state->timeline_history_exists_cb_copies)
+			return true;
+
 		/*
 		 * command apparently succeeded, but let's make sure the file is
 		 * really there now and has the correct size.
@@ -631,3 +651,65 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the restore callbacks into our global RestoreCallbacks.  The
+ * caller is responsible for validating the combination of library/command
+ * parameters that are set (e.g., restore_command and restore_library cannot
+ * both be set).
+ */
+void
+LoadRestoreCallbacks(void)
+{
+	RestoreModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_restore_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RestoreModuleInit)
+				load_external_function(restoreLibrary, "_PG_restore_module_init",
+									   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("restore modules have to define the symbol "
+						"_PG_restore_module_init")));
+
+	RestoreCallbacks = (*init) ();
+
+	/* restore state should be freed before calling this function */
+	Assert(restore_module_state == NULL);
+	restore_module_state = (RestoreModuleState *)
+							MemoryContextAllocZero(TopMemoryContext,
+												   sizeof(RestoreModuleState));
+
+	if (RestoreCallbacks->startup_cb != NULL)
+		RestoreCallbacks->startup_cb(restore_module_state);
+}
+
+/*
+ * Call the shutdown callback of the loaded restore module, if defined.  Also,
+ * free the restore module state if it was allocated.
+ *
+ * Processes that load restore libraries should register this via
+ * before_shmem_exit().
+ */
+void
+call_restore_module_shutdown_cb(int code, Datum arg)
+{
+	if (RestoreCallbacks != NULL &&
+		RestoreCallbacks->shutdown_cb != NULL &&
+		restore_module_state != NULL)
+		RestoreCallbacks->shutdown_cb(restore_module_state);
+
+	if (restore_module_state != NULL)
+	{
+		pfree(restore_module_state);
+		restore_module_state = NULL;
+	}
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index f0e1007f92..c6645c2413 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -50,6 +50,7 @@
 #include "postmaster/startup.h"
 #include "replication/slot.h"
 #include "replication/walreceiver.h"
+#include "restore/restore_module.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1078,18 +1079,21 @@ validateRecoveryParameters(void)
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			(!restore_wal_segment_configured() ||
+			 !restore_timeline_history_configured() ||
+			 !timeline_history_exists_configured()))
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
+					(errmsg("specified neither primary_conninfo nor restore_command nor a configured restore_library"),
 					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
 	}
 	else
 	{
-		if (recoveryRestoreCommand == NULL ||
-			strcmp(recoveryRestoreCommand, "") == 0)
+		if (!restore_wal_segment_configured() ||
+			!restore_timeline_history_configured() ||
+			!timeline_history_exists_configured())
 			ereport(FATAL,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("must specify restore_command when standby mode is not enabled")));
+					 errmsg("must specify restore_command or a configured restore_library when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index aaad5c5228..73ff592f9f 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -45,6 +45,7 @@
 #include "postmaster/bgwriter.h"
 #include "postmaster/interrupt.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -233,6 +234,24 @@ CheckpointerMain(void)
 												 ALLOCSET_DEFAULT_SIZES);
 	MemoryContextSwitchTo(checkpointer_context);
 
+	/*
+	 * Load restore_library so that we can use its archive-cleanup callback, if
+	 * one is defined.
+	 *
+	 * We also take this opportunity to set up the before_shmem_exit hook for
+	 * the shutdown callback and to check that only one of restore_library,
+	 * archive_cleanup_command is set.  Note that we emit a WARNING upon
+	 * detecting misconfigured parameters, and we clear the callbacks so that
+	 * the archive-cleanup functionality is skipped.  We judge this
+	 * functionality to not be important enough to require blocking checkpoints
+	 * or shutting down the server when the parameters are misconfigured.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+										 archiveCleanupCommand, "archive_cleanup_command",
+										 WARNING))
+		LoadRestoreCallbacks();
+
 	/*
 	 * If an exception is encountered, processing resumes here.
 	 *
@@ -548,6 +567,10 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		bool		archive_cleanup_settings_changed = false;
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -563,6 +586,36 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		/*
+		 * If the archive-cleanup settings have changed, call the currently
+		 * loaded shutdown callback and clear all the restore callbacks.
+		 * There's presently no good way to unload a library besides restarting
+		 * the process, and there should be little harm in leaving it around,
+		 * so we just leave it loaded.
+		 */
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			archive_cleanup_settings_changed = true;
+			call_restore_module_shutdown_cb(0, (Datum) 0);
+			RestoreCallbacks = NULL;
+		}
+
+		/*
+		 * As in CheckpointerMain(), we only emit a WARNING if we detect that
+		 * both restore_library and archive_cleanup_command are set.  We do
+		 * this even if the archive-cleanup settings haven't changed to remind
+		 * the user about the misconfiguration.
+		 */
+		if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											 archiveCleanupCommand, "archive_cleanup_command",
+											 WARNING) &&
+			archive_cleanup_settings_changed)
+			LoadRestoreCallbacks();
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index adce0ffcef..a096b79182 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -29,6 +29,7 @@
 #include "pgstat.h"
 #include "postmaster/interrupt.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
 #include "storage/pmsignal.h"
@@ -148,13 +149,17 @@ StartupProcShutdownHandler(SIGNAL_ARGS)
  * Re-read the config file.
  *
  * If one of the critical walreceiver options has changed, flag xlog.c
- * to restart it.
+ * to restart it.  Also, check for invalid combinations of the command/library
+ * parameters and reload the restore callbacks if necessary.
  */
 static void
 StartupRereadConfig(void)
 {
 	char	   *conninfo = pstrdup(PrimaryConnInfo);
 	char	   *slotname = pstrdup(PrimarySlotName);
+	char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+	char	   *prevRestoreCommand = pstrdup(recoveryRestoreCommand);
+	char	   *prevRecoveryEndCommand = pstrdup(recoveryEndCommand);
 	bool		tempSlot = wal_receiver_create_temp_slot;
 	bool		conninfoChanged;
 	bool		slotnameChanged;
@@ -176,6 +181,30 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	/*
+	 * If the restore settings have changed, call the currently loaded shutdown
+	 * callback and load the new callbacks.  There's presently no good way to
+	 * unload a library besides restarting the process, and there should be
+	 * little harm in leaving it around, so we just leave it loaded.
+	 */
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_restore_module_shutdown_cb(0, (Datum) 0);
+		LoadRestoreCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
@@ -285,6 +314,19 @@ StartupProcessMain(void)
 	 */
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	LoadRestoreCallbacks();
+
 	/*
 	 * Do what we came for.
 	 */
diff --git a/src/backend/restore/shell_restore.c b/src/backend/restore/shell_restore.c
index fcd5475c88..b67cd4ac48 100644
--- a/src/backend/restore/shell_restore.c
+++ b/src/backend/restore/shell_restore.c
@@ -3,7 +3,8 @@
  * shell_restore.c
  *
  * These restore functions use a user-specified shell command (e.g., the
- * restore_command GUC).
+ * restore_command GUC).  It is used as the default, but other modules may
+ * define their own restore logic.
  *
  * Copyright (c) 2023, PostgreSQL Global Development Group
  *
@@ -22,25 +23,76 @@
 #include "common/archive.h"
 #include "common/percentrepl.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static void shell_restore_startup(RestoreModuleState *state);
+static bool shell_restore_file_configured(RestoreModuleState *state);
 static bool shell_restore_file(const char *file, const char *path,
 							   const char *lastRestartPointFileName);
+static bool shell_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool shell_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool shell_archive_cleanup_configured(RestoreModuleState *state);
+static void shell_archive_cleanup(RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+static bool shell_recovery_end_configured(RestoreModuleState *state);
+static void shell_recovery_end(RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
 static void ExecuteRecoveryCommand(const char *command,
 								   const char *commandName,
 								   bool failOnSignal,
 								   uint32 wait_event_info,
 								   const char *lastRestartPointFileName);
 
+static const RestoreModuleCallbacks shell_restore_callbacks = {
+	.startup_cb = shell_restore_startup,
+	.restore_wal_segment_configured_cb = shell_restore_file_configured,
+	.restore_wal_segment_cb = shell_restore_wal_segment,
+	.restore_timeline_history_configured_cb = shell_restore_file_configured,
+	.restore_timeline_history_cb = shell_restore_timeline_history,
+	.timeline_history_exists_configured_cb = shell_restore_file_configured,
+	.timeline_history_exists_cb = shell_restore_timeline_history,
+	.archive_cleanup_configured_cb = shell_archive_cleanup_configured,
+	.archive_cleanup_cb = shell_archive_cleanup,
+	.recovery_end_configured_cb = shell_recovery_end_configured,
+	.recovery_end_cb = shell_recovery_end,
+	.shutdown_cb = NULL
+};
+
+/*
+ * shell_restore_init
+ *
+ * Returns the callbacks for restoring via shell.
+ */
+const RestoreModuleCallbacks *
+shell_restore_init(void)
+{
+	return &shell_restore_callbacks;
+}
+
+/*
+ * Indicates that our timeline_history_exists_cb only attempts to retrieve the
+ * file, so the caller should verify the file exists afterwards.
+ */
+static void
+shell_restore_startup(RestoreModuleState *state)
+{
+	state->timeline_history_exists_cb_copies = true;
+}
+
 /*
  * Attempt to execute a shell-based restore command to retrieve a WAL segment.
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_wal_segment(const char *file, const char *path,
+static bool
+shell_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
 						  const char *lastRestartPointFileName)
 {
 	return shell_restore_file(file, path, lastRestartPointFileName);
@@ -52,8 +104,9 @@ shell_restore_wal_segment(const char *file, const char *path,
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_timeline_history(const char *file, const char *path)
+static bool
+shell_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
 {
 	char		lastRestartPointFname[MAXPGPATH];
 
@@ -64,8 +117,8 @@ shell_restore_timeline_history(const char *file, const char *path)
 /*
  * Check whether restore_command is supplied.
  */
-bool
-shell_restore_file_configured(void)
+static bool
+shell_restore_file_configured(RestoreModuleState *state)
 {
 	return recoveryRestoreCommand[0] != '\0';
 }
@@ -153,8 +206,8 @@ shell_restore_file(const char *file, const char *path,
 /*
  * Check whether archive_cleanup_command is supplied.
  */
-bool
-shell_archive_cleanup_configured(void)
+static bool
+shell_archive_cleanup_configured(RestoreModuleState *state)
 {
 	return archiveCleanupCommand[0] != '\0';
 }
@@ -162,8 +215,9 @@ shell_archive_cleanup_configured(void)
 /*
  * Attempt to execute a shell-based archive cleanup command.
  */
-void
-shell_archive_cleanup(const char *lastRestartPointFileName)
+static void
+shell_archive_cleanup(RestoreModuleState *state,
+					  const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(archiveCleanupCommand, "archive_cleanup_command",
 						   false, WAIT_EVENT_ARCHIVE_CLEANUP_COMMAND,
@@ -173,8 +227,8 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 /*
  * Check whether recovery_end_command is supplied.
  */
-bool
-shell_recovery_end_configured(void)
+static bool
+shell_recovery_end_configured(RestoreModuleState *state)
 {
 	return recoveryEndCommand[0] != '\0';
 }
@@ -182,8 +236,9 @@ shell_recovery_end_configured(void)
 /*
  * Attempt to execute a shell-based end-of-recovery command.
  */
-void
-shell_recovery_end(const char *lastRestartPointFileName)
+static void
+shell_recovery_end(RestoreModuleState *state,
+				   const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(recoveryEndCommand, "recovery_end_command", true,
 						   WAIT_EVENT_RECOVERY_END_COMMAND,
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1c0583fe26..a42819399b 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -63,6 +63,7 @@
 #include "replication/logicallauncher.h"
 #include "replication/slot.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -3788,6 +3789,16 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"restore_library", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
+			gettext_noop("Sets the library that will be called for restore actions."),
+			NULL
+		},
+		&restoreLibrary,
+		"",
+		NULL, NULL, NULL
+	},
+
 	{
 		{"archive_cleanup_command", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
 			gettext_noop("Sets the shell command that will be executed at every restart point."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index d06074b86f..53757fea5f 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -270,6 +270,7 @@
 				# placeholders: %p = path of file to restore
 				#               %f = file name only
 				# e.g. 'cp /mnt/server/archivedir/%f %p'
+#restore_library = ''		# library to use for restore actions
 #archive_cleanup_command = ''	# command to execute at every restartpoint
 #recovery_end_command = ''	# command to execute at completion of recovery
 
diff --git a/src/include/restore/restore_module.h b/src/include/restore/restore_module.h
new file mode 100644
index 0000000000..c0aef33ba5
--- /dev/null
+++ b/src/include/restore/restore_module.h
@@ -0,0 +1,142 @@
+/*-------------------------------------------------------------------------
+ *
+ * restore_module.h
+ *		Exports for restore modules.
+ *
+ * Copyright (c) 2023, PostgreSQL Global Development Group
+ *
+ * src/include/restore/restore_module.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef _RESTORE_MODULE_H
+#define _RESTORE_MODULE_H
+
+/*
+ * The value of the restore_library GUC.
+ */
+extern PGDLLIMPORT char *restoreLibrary;
+
+typedef struct RestoreModuleState
+{
+	/*
+	 * Private data pointer for use by a restore module.  This can be used to
+	 * store state for the module that will be passed to each of its callbacks.
+	 */
+	void	   *private_data;
+
+	/*
+	 * Indicates whether the callback that checks for timeline existence merely
+	 * copies the file.  If set to true, the server will verify that the
+	 * timeline history file exists after the callback returns.
+	 */
+	bool		timeline_history_exists_cb_copies;
+} RestoreModuleState;
+
+/*
+ * Restore module callbacks
+ *
+ * These callback functions should be defined by restore libraries and returned
+ * via _PG_restore_module_init().  For more information about the purpose of
+ * each callback, refer to the restore modules documentation.
+ */
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state,
+									 const char *file, const char *path,
+									 const char *lastRestartPointFileName);
+typedef bool (*RestoreTimelineHistoryConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state,
+										  const char *file, const char *path);
+typedef bool (*TimelineHistoryExistsConfiguredCB) (RestoreModuleState *state);
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state,
+										 const char *file, const char *path);
+typedef bool (*ArchiveCleanupConfiguredCB) (RestoreModuleState *state);
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+typedef bool (*RecoveryEndConfiguredCB) (RestoreModuleState *state);
+typedef void (*RecoveryEndCB) (RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+
+typedef struct RestoreModuleCallbacks
+{
+	RestoreStartupCB startup_cb;
+	RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+	RestoreWalSegmentCB restore_wal_segment_cb;
+	RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+	RestoreTimelineHistoryCB restore_timeline_history_cb;
+	TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+	TimelineHistoryExistsCB timeline_history_exists_cb;
+	ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+	ArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndConfiguredCB recovery_end_configured_cb;
+	RecoveryEndCB recovery_end_cb;
+	RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+
+/*
+ * Type of the shared library symbol _PG_restore_module_init that is looked up
+ * when loading a restore library.
+ */
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+
+extern PGDLLEXPORT const RestoreModuleCallbacks *_PG_restore_module_init(void);
+
+extern void LoadRestoreCallbacks(void);
+extern void call_restore_module_shutdown_cb(int code, Datum arg);
+
+extern const RestoreModuleCallbacks *RestoreCallbacks;
+extern RestoreModuleState *restore_module_state;
+
+/*
+ * The following *_configured() functions are convenient wrappers around the
+ * *_configured_cb() callback functions with additional defensive NULL checks.
+ */
+
+static inline bool
+restore_wal_segment_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		   RestoreCallbacks->restore_wal_segment_configured_cb != NULL &&
+		   RestoreCallbacks->restore_wal_segment_cb != NULL &&
+		   RestoreCallbacks->restore_wal_segment_configured_cb(restore_module_state);
+}
+
+static inline bool
+restore_timeline_history_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		   RestoreCallbacks->restore_timeline_history_configured_cb != NULL &&
+		   RestoreCallbacks->restore_timeline_history_cb != NULL &&
+		   RestoreCallbacks->restore_timeline_history_configured_cb(restore_module_state);
+}
+
+static inline bool
+timeline_history_exists_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		   RestoreCallbacks->timeline_history_exists_configured_cb != NULL &&
+		   RestoreCallbacks->timeline_history_exists_cb != NULL &&
+		   RestoreCallbacks->timeline_history_exists_configured_cb(restore_module_state);
+}
+
+static inline bool
+archive_cleanup_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		   RestoreCallbacks->archive_cleanup_configured_cb != NULL &&
+		   RestoreCallbacks->archive_cleanup_cb != NULL &&
+		   RestoreCallbacks->archive_cleanup_configured_cb(restore_module_state);
+}
+
+static inline bool
+recovery_end_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		   RestoreCallbacks->recovery_end_configured_cb != NULL &&
+		   RestoreCallbacks->recovery_end_cb != NULL &&
+		   RestoreCallbacks->recovery_end_configured_cb(restore_module_state);
+}
+
+#endif							/* _RESTORE_MODULE_H */
diff --git a/src/include/restore/shell_restore.h b/src/include/restore/shell_restore.h
index 570588e493..1eeae51d81 100644
--- a/src/include/restore/shell_restore.h
+++ b/src/include/restore/shell_restore.h
@@ -12,15 +12,13 @@
 #ifndef _SHELL_RESTORE_H
 #define _SHELL_RESTORE_H
 
-extern bool shell_restore_file_configured(void);
-extern bool shell_restore_wal_segment(const char *file, const char *path,
-									  const char *lastRestartPointFileName);
-extern bool shell_restore_timeline_history(const char *file, const char *path);
+#include "restore/restore_module.h"
 
-extern bool shell_archive_cleanup_configured(void);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-
-extern bool shell_recovery_end_configured(void);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Since the logic for restoring via shell commands is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern const RestoreModuleCallbacks *shell_restore_init(void);
 
 #endif							/* _SHELL_RESTORE_H */
-- 
2.25.1


--IS0zKkzwUGydFO0o--





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

* [PATCH v18 5/5] introduce restore_library
@ 2023-02-16 06:06  Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 28+ messages in thread

From: Nathan Bossart @ 2023-02-16 06:06 UTC (permalink / raw)

---
 contrib/basic_archive/Makefile                |   2 +
 contrib/basic_archive/basic_archive.c         | 153 +++++++-
 contrib/basic_archive/meson.build             |   5 +
 contrib/basic_archive/t/001_restore.pl        |  44 +++
 doc/src/sgml/archive-and-restore-modules.sgml | 356 +++++++++++++++++-
 doc/src/sgml/backup.sgml                      |  40 +-
 doc/src/sgml/basic-archive.sgml               |  31 +-
 doc/src/sgml/config.sgml                      |  53 ++-
 doc/src/sgml/high-availability.sgml           |  18 +-
 src/backend/access/transam/xlog.c             |  20 +-
 src/backend/access/transam/xlogarchive.c      |  96 ++++-
 src/backend/access/transam/xlogrecovery.c     |  14 +-
 src/backend/postmaster/checkpointer.c         |  54 +++
 src/backend/postmaster/startup.c              |  44 ++-
 src/backend/restore/shell_restore.c           |  85 ++++-
 src/backend/utils/misc/guc_tables.c           |  11 +
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/restore/restore_module.h          | 143 +++++++
 src/include/restore/shell_restore.h           |  16 +-
 src/tools/pgindent/typedefs.list              |   2 +
 20 files changed, 1104 insertions(+), 84 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl
 create mode 100644 src/include/restore/restore_module.h

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 100ed81f12..e61f7d676f 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -10,6 +10,8 @@ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.c
 # typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
 
+TAP_TESTS = 1
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index 6b102e9072..e0d8dce364 100644
--- a/contrib/basic_archive/basic_archive.c
+++ b/contrib/basic_archive/basic_archive.c
@@ -17,6 +17,11 @@
  * a file is successfully archived and then the system crashes before
  * a durable record of the success has been made.
  *
+ * This file also demonstrates a basic restore library implementation that
+ * is roughly equivalent to the following shell command:
+ *
+ *		cp /path/to/archivedir/%f %p
+ *
  * Copyright (c) 2022-2024, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
@@ -33,6 +38,7 @@
 #include "archive/archive_module.h"
 #include "common/int.h"
 #include "miscadmin.h"
+#include "restore/restore_module.h"
 #include "storage/copydir.h"
 #include "storage/fd.h"
 #include "utils/guc.h"
@@ -54,6 +60,16 @@ static void basic_archive_file_internal(const char *file, const char *path);
 static bool check_archive_directory(char **newval, void **extra, GucSource source);
 static bool compare_files(const char *file1, const char *file2);
 static void basic_archive_shutdown(ArchiveModuleState *state);
+static bool basic_restore_configured(RestoreModuleState *state);
+static bool basic_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool basic_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool basic_restore_file(const char *file, const char *path);
+static bool basic_restore_timeline_history_exists(RestoreModuleState *state,
+												  const char *file,
+												  const char *path);
 
 static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.startup_cb = basic_archive_startup,
@@ -62,6 +78,21 @@ static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.shutdown_cb = basic_archive_shutdown
 };
 
+static const RestoreModuleCallbacks basic_restore_callbacks = {
+	.startup_cb = NULL,
+	.restore_wal_segment_configured_cb = basic_restore_configured,
+	.restore_wal_segment_cb = basic_restore_wal_segment,
+	.restore_timeline_history_configured_cb = basic_restore_configured,
+	.restore_timeline_history_cb = basic_restore_timeline_history,
+	.timeline_history_exists_configured_cb = basic_restore_configured,
+	.timeline_history_exists_cb = basic_restore_timeline_history_exists,
+	.archive_cleanup_configured_cb = NULL,
+	.archive_cleanup_cb = NULL,
+	.recovery_end_configured_cb = NULL,
+	.recovery_end_cb = NULL,
+	.shutdown_cb = NULL
+};
+
 /*
  * _PG_init
  *
@@ -71,7 +102,7 @@ void
 _PG_init(void)
 {
 	DefineCustomStringVariable("basic_archive.archive_directory",
-							   gettext_noop("Archive file destination directory."),
+							   gettext_noop("Archive file source/destination directory."),
 							   NULL,
 							   &archive_directory,
 							   "",
@@ -93,6 +124,17 @@ _PG_archive_module_init(void)
 	return &basic_archive_callbacks;
 }
 
+/*
+ * _PG_restore_module_init
+ *
+ * Returns the module's restore callbacks.
+ */
+const RestoreModuleCallbacks *
+_PG_restore_module_init(void)
+{
+	return &basic_restore_callbacks;
+}
+
 /*
  * basic_archive_startup
  *
@@ -431,3 +473,112 @@ basic_archive_shutdown(ArchiveModuleState *state)
 	pfree(data);
 	state->private_data = NULL;
 }
+
+/*
+ * basic_restore_configured
+ *
+ * Checks that archive_directory is not blank.
+ */
+static bool
+basic_restore_configured(RestoreModuleState *state)
+{
+	return archive_directory != NULL && archive_directory[0] != '\0';
+}
+
+/*
+ * basic_restore_wal_segment
+ *
+ * Retrieves one archived WAL segment.
+ */
+static bool
+basic_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
+						  const char *lastRestartPointFileName)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_timeline_history
+ *
+ * Retrieves one timeline history file.
+ */
+static bool
+basic_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file.
+ */
+static bool
+basic_restore_file(const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive",
+					file)));
+
+	/*
+	 * If the file doesn't exist, return false to indicate that there are no
+	 * more files to restore.
+	 */
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	copy_file(archive_path, path);
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive",
+					file)));
+	return true;
+}
+
+/*
+ * basic_restore_timeline_history_exists
+ *
+ * Check whether a timeline history file is present in the archive directory.
+ */
+static bool
+basic_restore_timeline_history_exists(RestoreModuleState *state,
+									  const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("checking existence of \"%s\" via basic_archive",
+					file)));
+
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	ereport(DEBUG1,
+			(errmsg("verified existence of \"%s\" via basic_archive",
+					file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index cc2f62bf36..8e36fa7ed6 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -31,4 +31,9 @@ tests += {
     # typical installcheck users do not have (e.g. buildfarm clients).
     'runningcheck': false,
   },
+  'tap': {
+    'tests': [
+      't/001_restore.pl',
+    ],
+  },
 }
diff --git a/contrib/basic_archive/t/001_restore.pl b/contrib/basic_archive/t/001_restore.pl
new file mode 100644
index 0000000000..6c01f736cf
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,44 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# start a node
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init(has_archiving => 1, allows_streaming => 1);
+my $archive_dir = $node->archive_dir;
+$archive_dir =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
+$node->append_conf('postgresql.conf', "archive_command = ''");
+$node->append_conf('postgresql.conf', "archive_library = 'basic_archive'");
+$node->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$node->start;
+
+# backup the node
+my $backup = 'backup';
+$node->backup($backup);
+
+# generate some new WAL files
+$node->safe_psql('postgres', "CREATE TABLE test (a INT);");
+$node->safe_psql('postgres', "SELECT pg_switch_wal();");
+$node->safe_psql('postgres', "INSERT INTO test VALUES (1);");
+
+# shut down the node (this should archive all WAL files)
+$node->stop;
+
+# restore from the backup
+my $restore = PostgreSQL::Test::Cluster->new('restore');
+$restore->init_from_backup($node, $backup, has_restoring => 1, standby => 0);
+$restore->append_conf('postgresql.conf', "restore_command = ''");
+$restore->append_conf('postgresql.conf', "restore_library = 'basic_archive'");
+$restore->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$restore->start;
+
+# ensure post-backup WAL is replayed
+my $result = $restore->poll_query_until("postgres", "SELECT count(*) = 1 FROM test;");
+is($result, "1", "check restore content");
+
+done_testing();
diff --git a/doc/src/sgml/archive-and-restore-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml
index a52d15082d..15be548a73 100644
--- a/doc/src/sgml/archive-and-restore-modules.sgml
+++ b/doc/src/sgml/archive-and-restore-modules.sgml
@@ -8,15 +8,17 @@
 
  <para>
   PostgreSQL provides infrastructure to create custom modules for continuous
-  archiving (see <xref linkend="continuous-archiving"/>).  While archiving via
-  a shell command (i.e., <xref linkend="guc-archive-command"/>) is much
-  simpler, a custom archive module will often be considerably more robust and
-  performant.
+  archiving and recovery (see <xref linkend="continuous-archiving"/>).  While a
+  shell command (i.e., <xref linkend="guc-archive-command"/>,
+  <xref linkend="guc-restore-command"/>) is much simpler, a custom module will
+  often be considerably more robust and performant.
  </para>
 
  <para>
   The <filename>contrib/basic_archive</filename> module contains a working
-  example, which demonstrates some useful techniques.
+  example, which demonstrates some useful techniques.  As demonstrated in
+  <filename>basic_archive</filename> a single module may serve as both an
+  archive module and a restore module.
  </para>
 
  <sect1 id="archive-modules">
@@ -178,4 +180,348 @@ typedef void (*ArchiveShutdownCB) (ArchiveModuleState *state);
    </sect3>
   </sect2>
  </sect1>
+
+ <sect1 id="restore-modules">
+  <title>Restore Modules</title>
+  <indexterm zone="restore-modules">
+   <primary>Restore Modules</primary>
+  </indexterm>
+
+  <para>
+   When a custom <xref linkend="guc-restore-library"/> is configured,
+   PostgreSQL will use the module for restore actions.  It is
+   ultimately up to the module to decide how to accomplish each task,
+   but some recommendations are listed at
+   <xref linkend="backup-pitr-recovery"/>.
+  </para>
+
+  <para>
+   Restore modules must at least consist of an initialization function
+   (see <xref linkend="restore-module-init"/>) and the required callbacks (see
+   <xref linkend="restore-module-callbacks"/>).  However, restore modules are
+   also permitted to do much more (e.g., declare GUCs and register background
+   workers).
+  </para>
+
+  <sect2 id="restore-module-init">
+   <title>Initialization Functions</title>
+   <indexterm zone="restore-module-init">
+    <primary>_PG_restore_module_init</primary>
+   </indexterm>
+
+   <para>
+    An restore library is loaded by dynamically loading a shared library with
+    the <xref linkend="guc-restore-library"/>'s name as the library base name.
+    The normal library search path is used to locate the library.  To provide
+    the required restore module callbacks and to indicate that the library is
+    actually a restore module, it needs to provide a function named
+    <function>_PG_restore_module_init</function>.  The result of the function
+    must be a pointer to a struct of type
+    <structname>RestoreModuleCallbacks</structname>, which contains everything
+    that the core code needs to know how to make use of the restore module.
+    The return value needs to be of server lifetime, which is typically
+    achieved by defining it as a <literal>static const</literal> variable in
+    global scope.
+
+<programlisting>
+typedef struct RestoreModuleCallbacks
+{
+    RestoreStartupCB startup_cb;
+    RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+    RestoreWalSegmentCB restore_wal_segment_cb;
+    RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+    RestoreTimelineHistoryCB restore_timeline_history_cb;
+    TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+    TimelineHistoryExistsCB timeline_history_exists_cb;
+    ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+    ArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndConfiguredCB recovery_end_configured_cb;
+    RecoveryEndCB recovery_end_cb;
+    RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+</programlisting>
+
+    The <function>restore_wal_segment_configured_cb</function>,
+    <function>restore_wal_segment_cb</function>,
+    <function>restore_timeline_history_configured_cb</function>,
+    <function>restore_timeline_history_cb</function>,
+    <function>timeline_history_exists_configured_cb</function>, and
+    <function>timeline_history_exists_cb</function> callbacks are required for
+    archive recovery but optional for streaming replication.  The others are
+    always optional.
+   </para>
+  </sect2>
+
+  <sect2 id="restore-module-callbacks">
+   <title>Restore Module Callbacks</title>
+
+   <para>
+    The restore callbacks define the actual behavior of the module.  The server
+    will call them as required to execute restore actions.
+   </para>
+
+   <sect3 id="restore-module-startup">
+    <title>Startup Callback</title>
+
+    <para>
+     The <function>startup_cb</function> callback is called shortly after the
+     module is loaded.  This callback can be used to perform any additional
+     initialization required.  If the restore module has state, it can use
+     <structfield>state->private_data</structfield> to store it.
+
+<programlisting>
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+</programlisting>
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment-configured">
+    <title>Restore WAL Segment Configured Callback</title>
+    <para>
+     The <function>restore_wal_segment_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to retrieve WAL files (e.g., its configuration parameters
+     are set to valid values).  If no
+     <function>restore_wal_segment_configured_cb</function> is defined, the
+     server always assumes the module is not configured to retrieve WAL files.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve WAL files
+     by calling the <function>restore_wal_segment_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_wal_segment_cb</function> callback to retrieve WAL
+     files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment">
+    <title>Restore WAL Segment Callback</title>
+    <para>
+     The <function>restore_wal_segment_cb</function> callback is called to
+     retrieve a single archived segment of the WAL file series for archive
+     recovery or streaming replication.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state, const char *file, const char *path, const char *lastRestartPointFileName);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point.  That is the earliest
+     file that must be kept to allow a restore to be restartable, so this
+     information can be used to truncate the archive to just the minimum
+     required to support restarting from the current restore.
+     <replaceable>lastRestartPointFileName</replaceable> is typically only used
+     by warm-standby configurations (see <xref linkend="warm-standby"/>).  Note
+     that if multiple standby servers are restoring from the same archive
+     directory, you will need to ensure that you do not delete WAL files until
+     they are no longer needed by any of the servers.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history-configured">
+    <title>Restore Timeline History Configured Callback</title>
+    <para>
+     The <function>restore_timeline_history_configured_cb</function> callback
+     is called to determine whether the module is fully configured and ready to
+     accept requests to retrieve timeline history files (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>restore_timeline_history_configured_cb</function> is defined,
+     the server always assumes the module is not configured to retrieve
+     timeline history files.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve timeline
+     history files by calling the
+     <function>restore_timeline_history_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_timeline_history_cb</function> callback to retrieve
+     timeline history files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history">
+    <title>Restore Timeline History Callback</title>
+    <para>
+     The <function>restore_timeline_history_cb</function> callback is called to
+     retrieve a single archived timeline history file for archive recovery or
+     streaming replication.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists-configured">
+    <title>Timeline History Exists Configured Callback</title>
+    <para>
+     The <function>timeline_history_exists_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to determine whether a timeline history file exists in the
+     archives (e.g., its configuration parameters are set to valid values).  If
+     no <function>timeline_history_exists_configured_cb</function> is defined,
+     the server always assumes the module is not configured to check whether
+     certain timeline history files exist.
+
+<programlisting>
+typedef bool (*TimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will check whether
+     certain timeline history files are present in the archives by calling the
+     <function>timeline_history_exists_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>timeline_history_exists_cb</function> callback to check if
+     timeline history files exist in the archives.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists">
+    <title>Timeline History Exists Callback</title>
+    <para>
+     The <function>timeline_history_exists_cb</function> callback is called to
+     check whether a timeline history file is present in the archives.
+
+<programlisting>
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file is
+     present in the archives.  If the file is not available in the archives,
+     the callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the timeline history file.
+    </para>
+
+    <para>
+     Some restore modules might not have a dedicated way to determine whether a
+     timeline history file exists.  For example,
+     <varname>restore_command</varname> only tells the server how to retrieve
+     files.  In this case, the <function>timeline_history_exists_cb</function>
+     callback should copy the file to <replaceable>path</replaceable>, which
+     contains the destination's relative path (including the file name), and
+     the restore module should set
+     <structfield>state->timeline_history_exists_cb_copies</structfield> to
+     <literal>true</literal>.  It is recommended to set this flag in the
+     module's <function>startup_cb</function> callback.  This flag tells the
+     server that it should verify that the file was successfully retrieved
+     after invoking this callback.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup-configured">
+    <title>Archive Cleanup Configured Callback</title>
+    <para>
+     The <function>archive_cleanup_configured_cb</function> callback is called
+     to determine whether the module is fully configured and ready to accept
+     requests to remove old WAL files from the archives (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>archive_cleanup_configured_cb</function> is defined, the server
+     always assumes the module is not configured to remove old WAL files.
+
+<programlisting>
+typedef bool (*ArchiveCleanupConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will remove old WAL
+     files by calling the <function>archive_cleanup_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>archive_cleanup_cb</function> callback to remove old WAL files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup">
+    <title>Archive Cleanup Callback</title>
+    <para>
+     The <function>archive_cleanup_cb</function> callback is called at every
+     restart point by the checkpointer process and is intended to provide a
+     mechanism for cleaning up old archived WAL files that are no longer
+     needed by the standby server.
+
+<programlisting>
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the
+     name of the file that includes the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end-configured">
+    <title>Recovery End Configured Callback</title>
+    <para>
+     The <function>recovery_end_configured_cb</function> callback is called to
+     determine whether the module is fully configured and ready to execute its
+     <function>recovery_end_cb</function> callback once at the end of recovery
+     (e.g., its configuration parameters are set to valid values).  If no
+     <function>recovery_end_configured_cb</function> is defined, the server
+     always assumes the module is not configured to execute its
+     <function>recovery_end_cb</function> callback.
+
+<programlisting>
+typedef bool (*RecoveryEndConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will execute the
+     <function>recovery_end_cb</function> callback once at the end of recovery.
+     If <literal>false</literal> is returned, the server will not use the
+     <function>recovery_end_cb</function> callback at the end of recovery.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end">
+    <title>Recovery End Callback</title>
+    <para>
+     The <function>recovery_end_cb</function> callback is called once at the
+     end of recovery and is intended to provide a mechanism for cleanup
+     following the end of replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-shutdown">
+    <title>Shutdown Callback</title>
+    <para>
+     The <function>shutdown_cb</function> callback is called when a process
+     that has loaded the restore module exits (e.g., after an error) or the
+     value of <xref linkend="guc-restore-library"/> changes.  If no
+     <function>shutdown_cb</function> is defined, no special action is taken in
+     these situations.  If the restore module has state, this callback should
+     free it to avoid leaks.
+
+<programlisting>
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+ </programlisting>
+    </para>
+   </sect3>
+  </sect2>
+ </sect1>
 </chapter>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index b3468eea3c..e1643f92e0 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1261,9 +1261,26 @@ SELECT * FROM pg_backup_stop(wait_for_archive => true);
    <para>
     The key part of all this is to set up a recovery configuration that
     describes how you want to recover and how far the recovery should
-    run.  The one thing that you absolutely must specify is the <varname>restore_command</varname>,
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>,
     which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    WAL file segments.
+   </para>
+
+   <para>
+    Like the <varname>archive_library</varname> parameter,
+    <varname>restore_library</varname> is a shared library.  Since such
+    libraries are written in <literal>C</literal>, creating your own may
+    require considerably more effort than writing a shell command.  However,
+    restore modules can be more performance than restoring via shell, and they
+    will have access to many useful server resources.  For more information
+    about creating a <varname>restore_library</varname>, see
+    <xref linkend="restore-modules"/>.
+   </para>
+
+   <para>
+    Like the <varname>archive_command</varname>,
+    <varname>restore_command</varname> is
     a shell command string.  It can contain <literal>%f</literal>, which is
     replaced by the name of the desired WAL file, and <literal>%p</literal>,
     which is replaced by the path name to copy the WAL file to.
@@ -1282,14 +1299,20 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
    </para>
 
    <para>
-    It is important that the command return nonzero exit status on failure.
-    The command <emphasis>will</emphasis> be called requesting files that are not
-    present in the archive; it must return nonzero when so asked.  This is not
-    an error condition.  An exception is that if the command was terminated by
+    It is important that the <varname>restore_command</varname> return nonzero
+    exit status on failure, or, if you are using a
+    <varname>restore_library</varname>, that the restore callbacks return
+    <literal>false</literal> on failure.  The command or library
+    <emphasis>will</emphasis> be called requesting files that are not
+    present in the archive; it must fail when so asked.  This is not
+    an error condition.  An exception is that if the
+    <varname>restore_command</varname> was terminated by
     a signal (other than <systemitem>SIGTERM</systemitem>, which is used as
     part of a database server shutdown) or an error by the shell (such as
     command not found), then recovery will abort and the server will not start
-    up.
+    up.  Likewise, if the restore callbacks provided by the
+    <varname>restore_library</varname> emit an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1313,7 +1336,8 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
     close as possible given the available WAL segments).  Therefore, a normal
     recovery will end with a <quote>file not found</quote> message, the exact text
     of the error message depending upon your choice of
-    <varname>restore_command</varname>.  You may also see an error message
+    <varname>restore_command</varname> or <varname>restore_library</varname>.
+    You may also see an error message
     at the start of recovery for a file named something like
     <filename>00000001.history</filename>.  This is also normal and does not
     indicate a problem in simple recovery situations; see
diff --git a/doc/src/sgml/basic-archive.sgml b/doc/src/sgml/basic-archive.sgml
index b4d43ced20..f3a5a502bd 100644
--- a/doc/src/sgml/basic-archive.sgml
+++ b/doc/src/sgml/basic-archive.sgml
@@ -8,17 +8,20 @@
  </indexterm>
 
  <para>
-  <filename>basic_archive</filename> is an example of an archive module.  This
-  module copies completed WAL segment files to the specified directory.  This
-  may not be especially useful, but it can serve as a starting point for
-  developing your own archive module.  For more information about archive
-  modules, see <xref linkend="archive-modules"/>.
+  <filename>basic_archive</filename> is an example of an archive and restore
+  module.  This module copies completed WAL segment files to or from the
+  specified directory.  This may not be especially useful, but it can serve as
+  a starting point for developing your own archive and restore modules.  For
+  more information about archive and restore modules, see\
+  <xref linkend="archive-and-restore-modules"/>.
  </para>
 
  <para>
-  In order to function, this module must be loaded via
+  For use as an archive module, this module must be loaded via
   <xref linkend="guc-archive-library"/>, and <xref linkend="guc-archive-mode"/>
-  must be enabled.
+  must be enabled.  For use as a restore module, this module must be loaded
+  via <xref linkend="guc-restore-library"/>, and recovery must be enabled (see
+  <xref linkend="runtime-config-wal-archive-recovery"/>).
  </para>
 
  <sect2 id="basic-archive-configuration-parameters">
@@ -34,11 +37,12 @@
     </term>
     <listitem>
      <para>
-      The directory where the server should copy WAL segment files.  This
-      directory must already exist.  The default is an empty string, which
-      effectively halts WAL archiving, but if <xref linkend="guc-archive-mode"/>
-      is enabled, the server will accumulate WAL segment files in the
-      expectation that a value will soon be provided.
+      The directory where the server should copy WAL segment files to or from.
+      This directory must already exist.  The default is an empty string,
+      which, when used for archiving, effectively halts WAL archival, but if
+      <xref linkend="guc-archive-mode"/> is enabled, the server will accumulate
+      WAL segment files in the expectation that a value will soon be provided.
+      When an empty string is used for recovery, restore will fail.
      </para>
     </listitem>
    </varlistentry>
@@ -46,7 +50,7 @@
 
   <para>
    These parameters must be set in <filename>postgresql.conf</filename>.
-   Typical usage might be:
+   Typical usage as an archive module might be:
   </para>
 
 <programlisting>
@@ -61,6 +65,7 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
+   When <filename>basic_archive</filename> is used as an archive module,
    Server crashes may leave temporary files with the prefix
    <filename>archtemp</filename> in the archive directory.  It is recommended to
    delete such files before restarting the server after a crash.  It is safe to
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index b38cbd714a..bba79e19ab 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3906,7 +3906,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
@@ -3934,7 +3935,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,
@@ -3969,7 +3971,42 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
 
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>restore_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-restore-library" xreflabel="restore_library">
+      <term><varname>restore_library</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>restore_library</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        The library to use for restore actions, including retrieving archived
+        files and executing tasks at restartpoints and at recovery end.  Either
+        <xref linkend="guc-restore-command"/> or
+        <varname>restore_library</varname> is required for archive recovery,
+        but optional for streaming replication.  If this parameter is set to an
+        empty string (the default), restoring via shell is enabled, and
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, and
+        <varname>recovery_end_command</varname> are used.  If both
+        <varname>restore_library</varname> and any of
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, or
+        <varname>recovery_end_command</varname> are set, an error will be
+        raised.  Otherwise, the specified shared library is used for restoring.
+        For more information, see <xref linkend="restore-modules"/>.
+       </para>
+
+       <para>
+        This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
        </para>
       </listitem>
      </varlistentry>
@@ -4014,7 +4051,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>
@@ -4043,7 +4083,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
        </para>
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>recovery_end_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index 236c0af65f..f811398fa2 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -627,7 +627,8 @@ protocol to make nodes agree on a serializable transactional order.
    <para>
     In standby mode, the server continuously applies WAL received from the
     primary server. The standby server can read WAL from a WAL archive
-    (see <xref linkend="guc-restore-command"/>) or directly from the primary
+    (see <xref linkend="guc-restore-command"/> and
+    <xref linkend="guc-restore-library"/>) or directly from the primary
     over a TCP connection (streaming replication). The standby server will
     also attempt to restore any WAL found in the standby cluster's
     <filename>pg_wal</filename> directory. That typically happens after a server
@@ -638,8 +639,10 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     At startup, the standby begins by restoring all WAL available in the
-    archive location, calling <varname>restore_command</varname>. Once it
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s callbacks.  Once it
     reaches the end of WAL available there and <varname>restore_command</varname>
+    or <varname>restore_library</varname>
     fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
     If that fails, and streaming replication has been configured, the
     standby tries to connect to the primary server and start streaming WAL
@@ -698,7 +701,8 @@ protocol to make nodes agree on a serializable transactional order.
     server (see <xref linkend="backup-pitr-recovery"/>). Create a file
     <link linkend="file-standby-signal"><filename>standby.signal</filename></link><indexterm><primary>standby.signal</primary></indexterm>
     in the standby's cluster data
-    directory. Set <xref linkend="guc-restore-command"/> to a simple command to copy files from
+    directory. Set <xref linkend="guc-restore-command"/> or
+    <xref linkend="guc-restore-library"/> to copy files from
     the WAL archive. If you plan to have multiple standby servers for high
     availability purposes, make sure that <varname>recovery_target_timeline</varname> is set to
     <literal>latest</literal> (the default), to make the standby server follow the timeline change
@@ -707,7 +711,8 @@ protocol to make nodes agree on a serializable transactional order.
 
    <note>
      <para>
-     <xref linkend="guc-restore-command"/> should return immediately
+     <xref linkend="guc-restore-command"/> and
+     <xref linkend="guc-restore-library"/> should return immediately
      if the file does not exist; the server will retry the command again if
      necessary.
     </para>
@@ -731,8 +736,9 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     If you're using a WAL archive, its size can be minimized using the <xref
-    linkend="guc-archive-cleanup-command"/> parameter to remove files that are no
-    longer required by the standby server.
+    linkend="guc-archive-cleanup-command"/> parameter or the
+    <xref linkend="guc-restore-library"/>'s archive cleanup callbacks to remove
+    files that are no longer required by the standby server.
     The <application>pg_archivecleanup</application> utility is designed specifically to
     be used with <varname>archive_cleanup_command</varname> in typical single-standby
     configurations, see <xref linkend="pgarchivecleanup"/>.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index e5d12f1d15..34a21c40a5 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -83,7 +83,7 @@
 #include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
-#include "restore/shell_restore.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
@@ -5178,18 +5178,19 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during
+	 * The callback is provided the archive file cutoff point for use during
 	 * log shipping replication.  All files earlier than this point can be
 	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_recovery_end_configured())
+	if (recovery_end_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_recovery_end(lastRestartPointFname);
+		RestoreCallbacks->recovery_end_cb(restore_module_state,
+										  lastRestartPointFname);
 	}
 
 	/*
@@ -7673,18 +7674,19 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Finally, execute archive-cleanup callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during
+	 * The callback is provided the archive file cutoff point for use during
 	 * log shipping replication.  All files earlier than this point can be
 	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_archive_cleanup_configured())
+	if (archive_cleanup_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_archive_cleanup(lastRestartPointFname);
+		RestoreCallbacks->archive_cleanup_cb(restore_module_state,
+											 lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index 320fa857ea..8ebbd6ce50 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -23,14 +23,21 @@
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
 #include "common/archive.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/startup.h"
 #include "postmaster/pgarch.h"
 #include "replication/walsender.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
+#include "utils/memutils.h"
+
+char	   *restoreLibrary = "";
+const RestoreModuleCallbacks *RestoreCallbacks = NULL;
+RestoreModuleState *restore_module_state;
 
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
@@ -74,15 +81,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			if (!shell_restore_file_configured())
+			if (!restore_wal_segment_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			if (!shell_restore_file_configured())
+			if (!restore_timeline_history_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			if (!shell_restore_file_configured())
+			if (!timeline_history_exists_configured())
 				goto not_available;
 			break;
 	}
@@ -174,14 +181,17 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			ret = shell_restore_wal_segment(xlogfname, xlogpath,
-											lastRestartPointFname);
+			ret = RestoreCallbacks->restore_wal_segment_cb(restore_module_state,
+														   xlogfname, xlogpath,
+														   lastRestartPointFname);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->restore_timeline_history_cb(restore_module_state,
+																xlogfname, xlogpath);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->timeline_history_exists_cb(restore_module_state,
+															   xlogfname, xlogpath);
 			break;
 	}
 
@@ -189,6 +199,16 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 
 	if (ret)
 	{
+		/*
+		 * Some restore functions might not copy the file (e.g., checking
+		 * whether a timeline history file exists), but they can set a flag to
+		 * tell us if they do.  We only need to verify file existence if this
+		 * flag is enabled.
+		 */
+		if (archive_type == ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS &&
+			!restore_module_state->timeline_history_exists_cb_copies)
+			return true;
+
 		/*
 		 * command apparently succeeded, but let's make sure the file is
 		 * really there now and has the correct size.
@@ -630,3 +650,65 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the restore callbacks into our global RestoreCallbacks.  The
+ * caller is responsible for validating the combination of library/command
+ * parameters that are set (e.g., restore_command and restore_library cannot
+ * both be set).
+ */
+void
+LoadRestoreCallbacks(void)
+{
+	RestoreModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_restore_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RestoreModuleInit)
+			load_external_function(restoreLibrary, "_PG_restore_module_init",
+								   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("restore modules have to define the symbol "
+						"_PG_restore_module_init")));
+
+	RestoreCallbacks = (*init) ();
+
+	/* restore state should be freed before calling this function */
+	Assert(restore_module_state == NULL);
+	restore_module_state = (RestoreModuleState *)
+		MemoryContextAllocZero(TopMemoryContext,
+							   sizeof(RestoreModuleState));
+
+	if (RestoreCallbacks->startup_cb != NULL)
+		RestoreCallbacks->startup_cb(restore_module_state);
+}
+
+/*
+ * Call the shutdown callback of the loaded restore module, if defined.  Also,
+ * free the restore module state if it was allocated.
+ *
+ * Processes that load restore libraries should register this via
+ * before_shmem_exit().
+ */
+void
+call_restore_module_shutdown_cb(int code, Datum arg)
+{
+	if (RestoreCallbacks != NULL &&
+		RestoreCallbacks->shutdown_cb != NULL &&
+		restore_module_state != NULL)
+		RestoreCallbacks->shutdown_cb(restore_module_state);
+
+	if (restore_module_state != NULL)
+	{
+		pfree(restore_module_state);
+		restore_module_state = NULL;
+	}
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 0bfa569502..4afdcc3aad 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -51,6 +51,7 @@
 #include "replication/slot.h"
 #include "replication/slotsync.h"
 #include "replication/walreceiver.h"
+#include "restore/restore_module.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1114,18 +1115,21 @@ validateRecoveryParameters(void)
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			(!restore_wal_segment_configured() ||
+			 !restore_timeline_history_configured() ||
+			 !timeline_history_exists_configured()))
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
+					(errmsg("specified neither primary_conninfo nor restore_command nor a configured restore_library"),
 					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
 	}
 	else
 	{
-		if (recoveryRestoreCommand == NULL ||
-			strcmp(recoveryRestoreCommand, "") == 0)
+		if (!restore_wal_segment_configured() ||
+			!restore_timeline_history_configured() ||
+			!timeline_history_exists_configured())
 			ereport(FATAL,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("must specify restore_command when standby mode is not enabled")));
+					 errmsg("must specify restore_command or a configured restore_library when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 46197d56f8..47993aa35e 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -45,6 +45,7 @@
 #include "postmaster/bgwriter.h"
 #include "postmaster/interrupt.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -224,6 +225,25 @@ CheckpointerMain(void)
 												 ALLOCSET_DEFAULT_SIZES);
 	MemoryContextSwitchTo(checkpointer_context);
 
+	/*
+	 * Load restore_library so that we can use its archive-cleanup callback,
+	 * if one is defined.
+	 *
+	 * We also take this opportunity to set up the before_shmem_exit hook for
+	 * the shutdown callback and to check that only one of restore_library,
+	 * archive_cleanup_command is set.  Note that we emit a WARNING upon
+	 * detecting misconfigured parameters, and we clear the callbacks so that
+	 * the archive-cleanup functionality is skipped.  We judge this
+	 * functionality to not be important enough to require blocking
+	 * checkpoints or shutting down the server when the parameters are
+	 * misconfigured.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+										 archiveCleanupCommand, "archive_cleanup_command",
+										 WARNING))
+		LoadRestoreCallbacks();
+
 	/*
 	 * If an exception is encountered, processing resumes here.
 	 *
@@ -556,6 +576,10 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		bool		archive_cleanup_settings_changed = false;
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -571,6 +595,36 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		/*
+		 * If the archive-cleanup settings have changed, call the currently
+		 * loaded shutdown callback and clear all the restore callbacks.
+		 * There's presently no good way to unload a library besides
+		 * restarting the process, and there should be little harm in leaving
+		 * it around, so we just leave it loaded.
+		 */
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			archive_cleanup_settings_changed = true;
+			call_restore_module_shutdown_cb(0, (Datum) 0);
+			RestoreCallbacks = NULL;
+		}
+
+		/*
+		 * As in CheckpointerMain(), we only emit a WARNING if we detect that
+		 * both restore_library and archive_cleanup_command are set.  We do
+		 * this even if the archive-cleanup settings haven't changed to remind
+		 * the user about the misconfiguration.
+		 */
+		if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											 archiveCleanupCommand, "archive_cleanup_command",
+											 WARNING) &&
+			archive_cleanup_settings_changed)
+			LoadRestoreCallbacks();
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index e391bf6aa6..3cbd382e00 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -25,6 +25,7 @@
 #include "libpq/pqsignal.h"
 #include "miscadmin.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "storage/ipc.h"
 #include "storage/pmsignal.h"
 #include "storage/procsignal.h"
@@ -118,13 +119,17 @@ StartupProcShutdownHandler(SIGNAL_ARGS)
  * Re-read the config file.
  *
  * If one of the critical walreceiver options has changed, flag xlog.c
- * to restart it.
+ * to restart it.  Also, check for invalid combinations of the command/library
+ * parameters and reload the restore callbacks if necessary.
  */
 static void
 StartupRereadConfig(void)
 {
 	char	   *conninfo = pstrdup(PrimaryConnInfo);
 	char	   *slotname = pstrdup(PrimarySlotName);
+	char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+	char	   *prevRestoreCommand = pstrdup(recoveryRestoreCommand);
+	char	   *prevRecoveryEndCommand = pstrdup(recoveryEndCommand);
 	bool		tempSlot = wal_receiver_create_temp_slot;
 	bool		conninfoChanged;
 	bool		slotnameChanged;
@@ -146,6 +151,30 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	/*
+	 * If the restore settings have changed, call the currently loaded
+	 * shutdown callback and load the new callbacks.  There's presently no
+	 * good way to unload a library besides restarting the process, and there
+	 * should be little harm in leaving it around, so we just leave it loaded.
+	 */
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_restore_module_shutdown_cb(0, (Datum) 0);
+		LoadRestoreCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
@@ -255,6 +284,19 @@ StartupProcessMain(void)
 	 */
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	LoadRestoreCallbacks();
+
 	/*
 	 * Do what we came for.
 	 */
diff --git a/src/backend/restore/shell_restore.c b/src/backend/restore/shell_restore.c
index 42291625c1..be25f04044 100644
--- a/src/backend/restore/shell_restore.c
+++ b/src/backend/restore/shell_restore.c
@@ -3,7 +3,8 @@
  * shell_restore.c
  *
  * These restore functions use a user-specified shell command (e.g., the
- * restore_command GUC).
+ * restore_command GUC).  It is used as the default, but other modules may
+ * define their own restore logic.
  *
  * Copyright (c) 2024, PostgreSQL Global Development Group
  *
@@ -22,25 +23,76 @@
 #include "common/archive.h"
 #include "common/percentrepl.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static void shell_restore_startup(RestoreModuleState *state);
+static bool shell_restore_file_configured(RestoreModuleState *state);
 static bool shell_restore_file(const char *file, const char *path,
 							   const char *lastRestartPointFileName);
+static bool shell_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool shell_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool shell_archive_cleanup_configured(RestoreModuleState *state);
+static void shell_archive_cleanup(RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+static bool shell_recovery_end_configured(RestoreModuleState *state);
+static void shell_recovery_end(RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
 static void ExecuteRecoveryCommand(const char *command,
 								   const char *commandName,
 								   bool failOnSignal,
 								   uint32 wait_event_info,
 								   const char *lastRestartPointFileName);
 
+static const RestoreModuleCallbacks shell_restore_callbacks = {
+	.startup_cb = shell_restore_startup,
+	.restore_wal_segment_configured_cb = shell_restore_file_configured,
+	.restore_wal_segment_cb = shell_restore_wal_segment,
+	.restore_timeline_history_configured_cb = shell_restore_file_configured,
+	.restore_timeline_history_cb = shell_restore_timeline_history,
+	.timeline_history_exists_configured_cb = shell_restore_file_configured,
+	.timeline_history_exists_cb = shell_restore_timeline_history,
+	.archive_cleanup_configured_cb = shell_archive_cleanup_configured,
+	.archive_cleanup_cb = shell_archive_cleanup,
+	.recovery_end_configured_cb = shell_recovery_end_configured,
+	.recovery_end_cb = shell_recovery_end,
+	.shutdown_cb = NULL
+};
+
+/*
+ * shell_restore_init
+ *
+ * Returns the callbacks for restoring via shell.
+ */
+const RestoreModuleCallbacks *
+shell_restore_init(void)
+{
+	return &shell_restore_callbacks;
+}
+
+/*
+ * Indicates that our timeline_history_exists_cb only attempts to retrieve the
+ * file, so the caller should verify the file exists afterwards.
+ */
+static void
+shell_restore_startup(RestoreModuleState *state)
+{
+	state->timeline_history_exists_cb_copies = true;
+}
+
 /*
  * Attempt to execute a shell-based restore command to retrieve a WAL segment.
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_wal_segment(const char *file, const char *path,
+static bool
+shell_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
 						  const char *lastRestartPointFileName)
 {
 	return shell_restore_file(file, path, lastRestartPointFileName);
@@ -52,8 +104,9 @@ shell_restore_wal_segment(const char *file, const char *path,
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_timeline_history(const char *file, const char *path)
+static bool
+shell_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
 {
 	char		lastRestartPointFname[MAXPGPATH];
 
@@ -64,8 +117,8 @@ shell_restore_timeline_history(const char *file, const char *path)
 /*
  * Check whether restore_command is supplied.
  */
-bool
-shell_restore_file_configured(void)
+static bool
+shell_restore_file_configured(RestoreModuleState *state)
 {
 	return recoveryRestoreCommand[0] != '\0';
 }
@@ -152,8 +205,8 @@ shell_restore_file(const char *file, const char *path,
 /*
  * Check whether archive_cleanup_command is supplied.
  */
-bool
-shell_archive_cleanup_configured(void)
+static bool
+shell_archive_cleanup_configured(RestoreModuleState *state)
 {
 	return archiveCleanupCommand[0] != '\0';
 }
@@ -161,8 +214,9 @@ shell_archive_cleanup_configured(void)
 /*
  * Attempt to execute a shell-based archive cleanup command.
  */
-void
-shell_archive_cleanup(const char *lastRestartPointFileName)
+static void
+shell_archive_cleanup(RestoreModuleState *state,
+					  const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(archiveCleanupCommand, "archive_cleanup_command",
 						   false, WAIT_EVENT_ARCHIVE_CLEANUP_COMMAND,
@@ -172,8 +226,8 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 /*
  * Check whether recovery_end_command is supplied.
  */
-bool
-shell_recovery_end_configured(void)
+static bool
+shell_recovery_end_configured(RestoreModuleState *state)
 {
 	return recoveryEndCommand[0] != '\0';
 }
@@ -181,8 +235,9 @@ shell_recovery_end_configured(void)
 /*
  * Attempt to execute a shell-based end-of-recovery command.
  */
-void
-shell_recovery_end(const char *lastRestartPointFileName)
+static void
+shell_recovery_end(RestoreModuleState *state,
+				   const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(recoveryEndCommand, "recovery_end_command", true,
 						   WAIT_EVENT_RECOVERY_END_COMMAND,
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 45013582a7..724d268d7b 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -70,6 +70,7 @@
 #include "replication/slot.h"
 #include "replication/slotsync.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -3946,6 +3947,16 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"restore_library", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
+			gettext_noop("Sets the library that will be called for restore actions."),
+			NULL
+		},
+		&restoreLibrary,
+		"",
+		NULL, NULL, NULL
+	},
+
 	{
 		{"archive_cleanup_command", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
 			gettext_noop("Sets the shell command that will be executed at every restart point."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index edcc0282b2..2939042b06 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -284,6 +284,7 @@
 				# placeholders: %p = path of file to restore
 				#               %f = file name only
 				# e.g. 'cp /mnt/server/archivedir/%f %p'
+#restore_library = ''		# library to use for restore actions
 #archive_cleanup_command = ''	# command to execute at every restartpoint
 #recovery_end_command = ''	# command to execute at completion of recovery
 
diff --git a/src/include/restore/restore_module.h b/src/include/restore/restore_module.h
new file mode 100644
index 0000000000..cc148e855a
--- /dev/null
+++ b/src/include/restore/restore_module.h
@@ -0,0 +1,143 @@
+/*-------------------------------------------------------------------------
+ *
+ * restore_module.h
+ *		Exports for restore modules.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * src/include/restore/restore_module.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef _RESTORE_MODULE_H
+#define _RESTORE_MODULE_H
+
+/*
+ * The value of the restore_library GUC.
+ */
+extern PGDLLIMPORT char *restoreLibrary;
+
+typedef struct RestoreModuleState
+{
+	/*
+	 * Private data pointer for use by a restore module.  This can be used to
+	 * store state for the module that will be passed to each of its
+	 * callbacks.
+	 */
+	void	   *private_data;
+
+	/*
+	 * Indicates whether the callback that checks for timeline existence
+	 * merely copies the file.  If set to true, the server will verify that
+	 * the timeline history file exists after the callback returns.
+	 */
+	bool		timeline_history_exists_cb_copies;
+} RestoreModuleState;
+
+/*
+ * Restore module callbacks
+ *
+ * These callback functions should be defined by restore libraries and returned
+ * via _PG_restore_module_init().  For more information about the purpose of
+ * each callback, refer to the restore modules documentation.
+ */
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state,
+									 const char *file, const char *path,
+									 const char *lastRestartPointFileName);
+typedef bool (*RestoreTimelineHistoryConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state,
+										  const char *file, const char *path);
+typedef bool (*TimelineHistoryExistsConfiguredCB) (RestoreModuleState *state);
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state,
+										 const char *file, const char *path);
+typedef bool (*ArchiveCleanupConfiguredCB) (RestoreModuleState *state);
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+typedef bool (*RecoveryEndConfiguredCB) (RestoreModuleState *state);
+typedef void (*RecoveryEndCB) (RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+
+typedef struct RestoreModuleCallbacks
+{
+	RestoreStartupCB startup_cb;
+	RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+	RestoreWalSegmentCB restore_wal_segment_cb;
+	RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+	RestoreTimelineHistoryCB restore_timeline_history_cb;
+	TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+	TimelineHistoryExistsCB timeline_history_exists_cb;
+	ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+	ArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndConfiguredCB recovery_end_configured_cb;
+	RecoveryEndCB recovery_end_cb;
+	RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+
+/*
+ * Type of the shared library symbol _PG_restore_module_init that is looked up
+ * when loading a restore library.
+ */
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+
+extern PGDLLEXPORT const RestoreModuleCallbacks *_PG_restore_module_init(void);
+
+extern void LoadRestoreCallbacks(void);
+extern void call_restore_module_shutdown_cb(int code, Datum arg);
+
+extern const RestoreModuleCallbacks *RestoreCallbacks;
+extern RestoreModuleState *restore_module_state;
+
+/*
+ * The following *_configured() functions are convenient wrappers around the
+ * *_configured_cb() callback functions with additional defensive NULL checks.
+ */
+
+static inline bool
+restore_wal_segment_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->restore_wal_segment_configured_cb != NULL &&
+		RestoreCallbacks->restore_wal_segment_cb != NULL &&
+		RestoreCallbacks->restore_wal_segment_configured_cb(restore_module_state);
+}
+
+static inline bool
+restore_timeline_history_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->restore_timeline_history_configured_cb != NULL &&
+		RestoreCallbacks->restore_timeline_history_cb != NULL &&
+		RestoreCallbacks->restore_timeline_history_configured_cb(restore_module_state);
+}
+
+static inline bool
+timeline_history_exists_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->timeline_history_exists_configured_cb != NULL &&
+		RestoreCallbacks->timeline_history_exists_cb != NULL &&
+		RestoreCallbacks->timeline_history_exists_configured_cb(restore_module_state);
+}
+
+static inline bool
+archive_cleanup_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->archive_cleanup_configured_cb != NULL &&
+		RestoreCallbacks->archive_cleanup_cb != NULL &&
+		RestoreCallbacks->archive_cleanup_configured_cb(restore_module_state);
+}
+
+static inline bool
+recovery_end_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->recovery_end_configured_cb != NULL &&
+		RestoreCallbacks->recovery_end_cb != NULL &&
+		RestoreCallbacks->recovery_end_configured_cb(restore_module_state);
+}
+
+#endif							/* _RESTORE_MODULE_H */
diff --git a/src/include/restore/shell_restore.h b/src/include/restore/shell_restore.h
index 28b5b7bc92..6352623866 100644
--- a/src/include/restore/shell_restore.h
+++ b/src/include/restore/shell_restore.h
@@ -12,15 +12,13 @@
 #ifndef _SHELL_RESTORE_H
 #define _SHELL_RESTORE_H
 
-extern bool shell_restore_file_configured(void);
-extern bool shell_restore_wal_segment(const char *file, const char *path,
-									  const char *lastRestartPointFileName);
-extern bool shell_restore_timeline_history(const char *file, const char *path);
+#include "restore/restore_module.h"
 
-extern bool shell_archive_cleanup_configured(void);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-
-extern bool shell_recovery_end_configured(void);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Since the logic for restoring via shell commands is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern const RestoreModuleCallbacks *shell_restore_init(void);
 
 #endif							/* _SHELL_RESTORE_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3a02310f26..6a05313f6b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2386,6 +2386,8 @@ ResourceOwnerDesc
 ResourceReleaseCallback
 ResourceReleaseCallbackItem
 ResourceReleasePhase
+RestoreModuleCallbacks
+RestoreModuleState
 RestoreOptions
 RestorePass
 RestrictInfo
-- 
2.25.1


--IJpNTDwzlM2Ie8A6--





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

* [PATCH v19 5/5] introduce restore_library
@ 2023-02-16 06:06  Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 28+ messages in thread

From: Nathan Bossart @ 2023-02-16 06:06 UTC (permalink / raw)

---
 contrib/basic_archive/Makefile                |   2 +
 contrib/basic_archive/basic_archive.c         | 153 +++++++-
 contrib/basic_archive/meson.build             |   5 +
 contrib/basic_archive/t/001_restore.pl        |  44 +++
 doc/src/sgml/archive-and-restore-modules.sgml | 356 +++++++++++++++++-
 doc/src/sgml/backup.sgml                      |  40 +-
 doc/src/sgml/basic-archive.sgml               |  31 +-
 doc/src/sgml/config.sgml                      |  53 ++-
 doc/src/sgml/high-availability.sgml           |  18 +-
 src/backend/access/transam/xlog.c             |  20 +-
 src/backend/access/transam/xlogarchive.c      |  96 ++++-
 src/backend/access/transam/xlogrecovery.c     |  14 +-
 src/backend/postmaster/checkpointer.c         |  54 +++
 src/backend/postmaster/startup.c              |  44 ++-
 src/backend/restore/shell_restore.c           |  85 ++++-
 src/backend/utils/misc/guc_tables.c           |  11 +
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/restore/restore_module.h          | 143 +++++++
 src/include/restore/shell_restore.h           |  16 +-
 src/tools/pgindent/typedefs.list              |   2 +
 20 files changed, 1104 insertions(+), 84 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl
 create mode 100644 src/include/restore/restore_module.h

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 100ed81f12..e61f7d676f 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -10,6 +10,8 @@ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.c
 # typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
 
+TAP_TESTS = 1
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index 6b102e9072..e0d8dce364 100644
--- a/contrib/basic_archive/basic_archive.c
+++ b/contrib/basic_archive/basic_archive.c
@@ -17,6 +17,11 @@
  * a file is successfully archived and then the system crashes before
  * a durable record of the success has been made.
  *
+ * This file also demonstrates a basic restore library implementation that
+ * is roughly equivalent to the following shell command:
+ *
+ *		cp /path/to/archivedir/%f %p
+ *
  * Copyright (c) 2022-2024, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
@@ -33,6 +38,7 @@
 #include "archive/archive_module.h"
 #include "common/int.h"
 #include "miscadmin.h"
+#include "restore/restore_module.h"
 #include "storage/copydir.h"
 #include "storage/fd.h"
 #include "utils/guc.h"
@@ -54,6 +60,16 @@ static void basic_archive_file_internal(const char *file, const char *path);
 static bool check_archive_directory(char **newval, void **extra, GucSource source);
 static bool compare_files(const char *file1, const char *file2);
 static void basic_archive_shutdown(ArchiveModuleState *state);
+static bool basic_restore_configured(RestoreModuleState *state);
+static bool basic_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool basic_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool basic_restore_file(const char *file, const char *path);
+static bool basic_restore_timeline_history_exists(RestoreModuleState *state,
+												  const char *file,
+												  const char *path);
 
 static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.startup_cb = basic_archive_startup,
@@ -62,6 +78,21 @@ static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.shutdown_cb = basic_archive_shutdown
 };
 
+static const RestoreModuleCallbacks basic_restore_callbacks = {
+	.startup_cb = NULL,
+	.restore_wal_segment_configured_cb = basic_restore_configured,
+	.restore_wal_segment_cb = basic_restore_wal_segment,
+	.restore_timeline_history_configured_cb = basic_restore_configured,
+	.restore_timeline_history_cb = basic_restore_timeline_history,
+	.timeline_history_exists_configured_cb = basic_restore_configured,
+	.timeline_history_exists_cb = basic_restore_timeline_history_exists,
+	.archive_cleanup_configured_cb = NULL,
+	.archive_cleanup_cb = NULL,
+	.recovery_end_configured_cb = NULL,
+	.recovery_end_cb = NULL,
+	.shutdown_cb = NULL
+};
+
 /*
  * _PG_init
  *
@@ -71,7 +102,7 @@ void
 _PG_init(void)
 {
 	DefineCustomStringVariable("basic_archive.archive_directory",
-							   gettext_noop("Archive file destination directory."),
+							   gettext_noop("Archive file source/destination directory."),
 							   NULL,
 							   &archive_directory,
 							   "",
@@ -93,6 +124,17 @@ _PG_archive_module_init(void)
 	return &basic_archive_callbacks;
 }
 
+/*
+ * _PG_restore_module_init
+ *
+ * Returns the module's restore callbacks.
+ */
+const RestoreModuleCallbacks *
+_PG_restore_module_init(void)
+{
+	return &basic_restore_callbacks;
+}
+
 /*
  * basic_archive_startup
  *
@@ -431,3 +473,112 @@ basic_archive_shutdown(ArchiveModuleState *state)
 	pfree(data);
 	state->private_data = NULL;
 }
+
+/*
+ * basic_restore_configured
+ *
+ * Checks that archive_directory is not blank.
+ */
+static bool
+basic_restore_configured(RestoreModuleState *state)
+{
+	return archive_directory != NULL && archive_directory[0] != '\0';
+}
+
+/*
+ * basic_restore_wal_segment
+ *
+ * Retrieves one archived WAL segment.
+ */
+static bool
+basic_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
+						  const char *lastRestartPointFileName)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_timeline_history
+ *
+ * Retrieves one timeline history file.
+ */
+static bool
+basic_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file.
+ */
+static bool
+basic_restore_file(const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive",
+					file)));
+
+	/*
+	 * If the file doesn't exist, return false to indicate that there are no
+	 * more files to restore.
+	 */
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	copy_file(archive_path, path);
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive",
+					file)));
+	return true;
+}
+
+/*
+ * basic_restore_timeline_history_exists
+ *
+ * Check whether a timeline history file is present in the archive directory.
+ */
+static bool
+basic_restore_timeline_history_exists(RestoreModuleState *state,
+									  const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("checking existence of \"%s\" via basic_archive",
+					file)));
+
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	ereport(DEBUG1,
+			(errmsg("verified existence of \"%s\" via basic_archive",
+					file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index cc2f62bf36..8e36fa7ed6 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -31,4 +31,9 @@ tests += {
     # typical installcheck users do not have (e.g. buildfarm clients).
     'runningcheck': false,
   },
+  'tap': {
+    'tests': [
+      't/001_restore.pl',
+    ],
+  },
 }
diff --git a/contrib/basic_archive/t/001_restore.pl b/contrib/basic_archive/t/001_restore.pl
new file mode 100644
index 0000000000..6c01f736cf
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,44 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# start a node
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init(has_archiving => 1, allows_streaming => 1);
+my $archive_dir = $node->archive_dir;
+$archive_dir =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
+$node->append_conf('postgresql.conf', "archive_command = ''");
+$node->append_conf('postgresql.conf', "archive_library = 'basic_archive'");
+$node->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$node->start;
+
+# backup the node
+my $backup = 'backup';
+$node->backup($backup);
+
+# generate some new WAL files
+$node->safe_psql('postgres', "CREATE TABLE test (a INT);");
+$node->safe_psql('postgres', "SELECT pg_switch_wal();");
+$node->safe_psql('postgres', "INSERT INTO test VALUES (1);");
+
+# shut down the node (this should archive all WAL files)
+$node->stop;
+
+# restore from the backup
+my $restore = PostgreSQL::Test::Cluster->new('restore');
+$restore->init_from_backup($node, $backup, has_restoring => 1, standby => 0);
+$restore->append_conf('postgresql.conf', "restore_command = ''");
+$restore->append_conf('postgresql.conf', "restore_library = 'basic_archive'");
+$restore->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$restore->start;
+
+# ensure post-backup WAL is replayed
+my $result = $restore->poll_query_until("postgres", "SELECT count(*) = 1 FROM test;");
+is($result, "1", "check restore content");
+
+done_testing();
diff --git a/doc/src/sgml/archive-and-restore-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml
index a52d15082d..15be548a73 100644
--- a/doc/src/sgml/archive-and-restore-modules.sgml
+++ b/doc/src/sgml/archive-and-restore-modules.sgml
@@ -8,15 +8,17 @@
 
  <para>
   PostgreSQL provides infrastructure to create custom modules for continuous
-  archiving (see <xref linkend="continuous-archiving"/>).  While archiving via
-  a shell command (i.e., <xref linkend="guc-archive-command"/>) is much
-  simpler, a custom archive module will often be considerably more robust and
-  performant.
+  archiving and recovery (see <xref linkend="continuous-archiving"/>).  While a
+  shell command (i.e., <xref linkend="guc-archive-command"/>,
+  <xref linkend="guc-restore-command"/>) is much simpler, a custom module will
+  often be considerably more robust and performant.
  </para>
 
  <para>
   The <filename>contrib/basic_archive</filename> module contains a working
-  example, which demonstrates some useful techniques.
+  example, which demonstrates some useful techniques.  As demonstrated in
+  <filename>basic_archive</filename> a single module may serve as both an
+  archive module and a restore module.
  </para>
 
  <sect1 id="archive-modules">
@@ -178,4 +180,348 @@ typedef void (*ArchiveShutdownCB) (ArchiveModuleState *state);
    </sect3>
   </sect2>
  </sect1>
+
+ <sect1 id="restore-modules">
+  <title>Restore Modules</title>
+  <indexterm zone="restore-modules">
+   <primary>Restore Modules</primary>
+  </indexterm>
+
+  <para>
+   When a custom <xref linkend="guc-restore-library"/> is configured,
+   PostgreSQL will use the module for restore actions.  It is
+   ultimately up to the module to decide how to accomplish each task,
+   but some recommendations are listed at
+   <xref linkend="backup-pitr-recovery"/>.
+  </para>
+
+  <para>
+   Restore modules must at least consist of an initialization function
+   (see <xref linkend="restore-module-init"/>) and the required callbacks (see
+   <xref linkend="restore-module-callbacks"/>).  However, restore modules are
+   also permitted to do much more (e.g., declare GUCs and register background
+   workers).
+  </para>
+
+  <sect2 id="restore-module-init">
+   <title>Initialization Functions</title>
+   <indexterm zone="restore-module-init">
+    <primary>_PG_restore_module_init</primary>
+   </indexterm>
+
+   <para>
+    An restore library is loaded by dynamically loading a shared library with
+    the <xref linkend="guc-restore-library"/>'s name as the library base name.
+    The normal library search path is used to locate the library.  To provide
+    the required restore module callbacks and to indicate that the library is
+    actually a restore module, it needs to provide a function named
+    <function>_PG_restore_module_init</function>.  The result of the function
+    must be a pointer to a struct of type
+    <structname>RestoreModuleCallbacks</structname>, which contains everything
+    that the core code needs to know how to make use of the restore module.
+    The return value needs to be of server lifetime, which is typically
+    achieved by defining it as a <literal>static const</literal> variable in
+    global scope.
+
+<programlisting>
+typedef struct RestoreModuleCallbacks
+{
+    RestoreStartupCB startup_cb;
+    RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+    RestoreWalSegmentCB restore_wal_segment_cb;
+    RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+    RestoreTimelineHistoryCB restore_timeline_history_cb;
+    TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+    TimelineHistoryExistsCB timeline_history_exists_cb;
+    ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+    ArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndConfiguredCB recovery_end_configured_cb;
+    RecoveryEndCB recovery_end_cb;
+    RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+</programlisting>
+
+    The <function>restore_wal_segment_configured_cb</function>,
+    <function>restore_wal_segment_cb</function>,
+    <function>restore_timeline_history_configured_cb</function>,
+    <function>restore_timeline_history_cb</function>,
+    <function>timeline_history_exists_configured_cb</function>, and
+    <function>timeline_history_exists_cb</function> callbacks are required for
+    archive recovery but optional for streaming replication.  The others are
+    always optional.
+   </para>
+  </sect2>
+
+  <sect2 id="restore-module-callbacks">
+   <title>Restore Module Callbacks</title>
+
+   <para>
+    The restore callbacks define the actual behavior of the module.  The server
+    will call them as required to execute restore actions.
+   </para>
+
+   <sect3 id="restore-module-startup">
+    <title>Startup Callback</title>
+
+    <para>
+     The <function>startup_cb</function> callback is called shortly after the
+     module is loaded.  This callback can be used to perform any additional
+     initialization required.  If the restore module has state, it can use
+     <structfield>state->private_data</structfield> to store it.
+
+<programlisting>
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+</programlisting>
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment-configured">
+    <title>Restore WAL Segment Configured Callback</title>
+    <para>
+     The <function>restore_wal_segment_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to retrieve WAL files (e.g., its configuration parameters
+     are set to valid values).  If no
+     <function>restore_wal_segment_configured_cb</function> is defined, the
+     server always assumes the module is not configured to retrieve WAL files.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve WAL files
+     by calling the <function>restore_wal_segment_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_wal_segment_cb</function> callback to retrieve WAL
+     files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment">
+    <title>Restore WAL Segment Callback</title>
+    <para>
+     The <function>restore_wal_segment_cb</function> callback is called to
+     retrieve a single archived segment of the WAL file series for archive
+     recovery or streaming replication.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state, const char *file, const char *path, const char *lastRestartPointFileName);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point.  That is the earliest
+     file that must be kept to allow a restore to be restartable, so this
+     information can be used to truncate the archive to just the minimum
+     required to support restarting from the current restore.
+     <replaceable>lastRestartPointFileName</replaceable> is typically only used
+     by warm-standby configurations (see <xref linkend="warm-standby"/>).  Note
+     that if multiple standby servers are restoring from the same archive
+     directory, you will need to ensure that you do not delete WAL files until
+     they are no longer needed by any of the servers.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history-configured">
+    <title>Restore Timeline History Configured Callback</title>
+    <para>
+     The <function>restore_timeline_history_configured_cb</function> callback
+     is called to determine whether the module is fully configured and ready to
+     accept requests to retrieve timeline history files (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>restore_timeline_history_configured_cb</function> is defined,
+     the server always assumes the module is not configured to retrieve
+     timeline history files.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve timeline
+     history files by calling the
+     <function>restore_timeline_history_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_timeline_history_cb</function> callback to retrieve
+     timeline history files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history">
+    <title>Restore Timeline History Callback</title>
+    <para>
+     The <function>restore_timeline_history_cb</function> callback is called to
+     retrieve a single archived timeline history file for archive recovery or
+     streaming replication.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists-configured">
+    <title>Timeline History Exists Configured Callback</title>
+    <para>
+     The <function>timeline_history_exists_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to determine whether a timeline history file exists in the
+     archives (e.g., its configuration parameters are set to valid values).  If
+     no <function>timeline_history_exists_configured_cb</function> is defined,
+     the server always assumes the module is not configured to check whether
+     certain timeline history files exist.
+
+<programlisting>
+typedef bool (*TimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will check whether
+     certain timeline history files are present in the archives by calling the
+     <function>timeline_history_exists_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>timeline_history_exists_cb</function> callback to check if
+     timeline history files exist in the archives.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists">
+    <title>Timeline History Exists Callback</title>
+    <para>
+     The <function>timeline_history_exists_cb</function> callback is called to
+     check whether a timeline history file is present in the archives.
+
+<programlisting>
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file is
+     present in the archives.  If the file is not available in the archives,
+     the callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the timeline history file.
+    </para>
+
+    <para>
+     Some restore modules might not have a dedicated way to determine whether a
+     timeline history file exists.  For example,
+     <varname>restore_command</varname> only tells the server how to retrieve
+     files.  In this case, the <function>timeline_history_exists_cb</function>
+     callback should copy the file to <replaceable>path</replaceable>, which
+     contains the destination's relative path (including the file name), and
+     the restore module should set
+     <structfield>state->timeline_history_exists_cb_copies</structfield> to
+     <literal>true</literal>.  It is recommended to set this flag in the
+     module's <function>startup_cb</function> callback.  This flag tells the
+     server that it should verify that the file was successfully retrieved
+     after invoking this callback.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup-configured">
+    <title>Archive Cleanup Configured Callback</title>
+    <para>
+     The <function>archive_cleanup_configured_cb</function> callback is called
+     to determine whether the module is fully configured and ready to accept
+     requests to remove old WAL files from the archives (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>archive_cleanup_configured_cb</function> is defined, the server
+     always assumes the module is not configured to remove old WAL files.
+
+<programlisting>
+typedef bool (*ArchiveCleanupConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will remove old WAL
+     files by calling the <function>archive_cleanup_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>archive_cleanup_cb</function> callback to remove old WAL files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup">
+    <title>Archive Cleanup Callback</title>
+    <para>
+     The <function>archive_cleanup_cb</function> callback is called at every
+     restart point by the checkpointer process and is intended to provide a
+     mechanism for cleaning up old archived WAL files that are no longer
+     needed by the standby server.
+
+<programlisting>
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the
+     name of the file that includes the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end-configured">
+    <title>Recovery End Configured Callback</title>
+    <para>
+     The <function>recovery_end_configured_cb</function> callback is called to
+     determine whether the module is fully configured and ready to execute its
+     <function>recovery_end_cb</function> callback once at the end of recovery
+     (e.g., its configuration parameters are set to valid values).  If no
+     <function>recovery_end_configured_cb</function> is defined, the server
+     always assumes the module is not configured to execute its
+     <function>recovery_end_cb</function> callback.
+
+<programlisting>
+typedef bool (*RecoveryEndConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will execute the
+     <function>recovery_end_cb</function> callback once at the end of recovery.
+     If <literal>false</literal> is returned, the server will not use the
+     <function>recovery_end_cb</function> callback at the end of recovery.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end">
+    <title>Recovery End Callback</title>
+    <para>
+     The <function>recovery_end_cb</function> callback is called once at the
+     end of recovery and is intended to provide a mechanism for cleanup
+     following the end of replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-shutdown">
+    <title>Shutdown Callback</title>
+    <para>
+     The <function>shutdown_cb</function> callback is called when a process
+     that has loaded the restore module exits (e.g., after an error) or the
+     value of <xref linkend="guc-restore-library"/> changes.  If no
+     <function>shutdown_cb</function> is defined, no special action is taken in
+     these situations.  If the restore module has state, this callback should
+     free it to avoid leaks.
+
+<programlisting>
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+ </programlisting>
+    </para>
+   </sect3>
+  </sect2>
+ </sect1>
 </chapter>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index b3468eea3c..e1643f92e0 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1261,9 +1261,26 @@ SELECT * FROM pg_backup_stop(wait_for_archive => true);
    <para>
     The key part of all this is to set up a recovery configuration that
     describes how you want to recover and how far the recovery should
-    run.  The one thing that you absolutely must specify is the <varname>restore_command</varname>,
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>,
     which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    WAL file segments.
+   </para>
+
+   <para>
+    Like the <varname>archive_library</varname> parameter,
+    <varname>restore_library</varname> is a shared library.  Since such
+    libraries are written in <literal>C</literal>, creating your own may
+    require considerably more effort than writing a shell command.  However,
+    restore modules can be more performance than restoring via shell, and they
+    will have access to many useful server resources.  For more information
+    about creating a <varname>restore_library</varname>, see
+    <xref linkend="restore-modules"/>.
+   </para>
+
+   <para>
+    Like the <varname>archive_command</varname>,
+    <varname>restore_command</varname> is
     a shell command string.  It can contain <literal>%f</literal>, which is
     replaced by the name of the desired WAL file, and <literal>%p</literal>,
     which is replaced by the path name to copy the WAL file to.
@@ -1282,14 +1299,20 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
    </para>
 
    <para>
-    It is important that the command return nonzero exit status on failure.
-    The command <emphasis>will</emphasis> be called requesting files that are not
-    present in the archive; it must return nonzero when so asked.  This is not
-    an error condition.  An exception is that if the command was terminated by
+    It is important that the <varname>restore_command</varname> return nonzero
+    exit status on failure, or, if you are using a
+    <varname>restore_library</varname>, that the restore callbacks return
+    <literal>false</literal> on failure.  The command or library
+    <emphasis>will</emphasis> be called requesting files that are not
+    present in the archive; it must fail when so asked.  This is not
+    an error condition.  An exception is that if the
+    <varname>restore_command</varname> was terminated by
     a signal (other than <systemitem>SIGTERM</systemitem>, which is used as
     part of a database server shutdown) or an error by the shell (such as
     command not found), then recovery will abort and the server will not start
-    up.
+    up.  Likewise, if the restore callbacks provided by the
+    <varname>restore_library</varname> emit an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1313,7 +1336,8 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
     close as possible given the available WAL segments).  Therefore, a normal
     recovery will end with a <quote>file not found</quote> message, the exact text
     of the error message depending upon your choice of
-    <varname>restore_command</varname>.  You may also see an error message
+    <varname>restore_command</varname> or <varname>restore_library</varname>.
+    You may also see an error message
     at the start of recovery for a file named something like
     <filename>00000001.history</filename>.  This is also normal and does not
     indicate a problem in simple recovery situations; see
diff --git a/doc/src/sgml/basic-archive.sgml b/doc/src/sgml/basic-archive.sgml
index b4d43ced20..f3a5a502bd 100644
--- a/doc/src/sgml/basic-archive.sgml
+++ b/doc/src/sgml/basic-archive.sgml
@@ -8,17 +8,20 @@
  </indexterm>
 
  <para>
-  <filename>basic_archive</filename> is an example of an archive module.  This
-  module copies completed WAL segment files to the specified directory.  This
-  may not be especially useful, but it can serve as a starting point for
-  developing your own archive module.  For more information about archive
-  modules, see <xref linkend="archive-modules"/>.
+  <filename>basic_archive</filename> is an example of an archive and restore
+  module.  This module copies completed WAL segment files to or from the
+  specified directory.  This may not be especially useful, but it can serve as
+  a starting point for developing your own archive and restore modules.  For
+  more information about archive and restore modules, see\
+  <xref linkend="archive-and-restore-modules"/>.
  </para>
 
  <para>
-  In order to function, this module must be loaded via
+  For use as an archive module, this module must be loaded via
   <xref linkend="guc-archive-library"/>, and <xref linkend="guc-archive-mode"/>
-  must be enabled.
+  must be enabled.  For use as a restore module, this module must be loaded
+  via <xref linkend="guc-restore-library"/>, and recovery must be enabled (see
+  <xref linkend="runtime-config-wal-archive-recovery"/>).
  </para>
 
  <sect2 id="basic-archive-configuration-parameters">
@@ -34,11 +37,12 @@
     </term>
     <listitem>
      <para>
-      The directory where the server should copy WAL segment files.  This
-      directory must already exist.  The default is an empty string, which
-      effectively halts WAL archiving, but if <xref linkend="guc-archive-mode"/>
-      is enabled, the server will accumulate WAL segment files in the
-      expectation that a value will soon be provided.
+      The directory where the server should copy WAL segment files to or from.
+      This directory must already exist.  The default is an empty string,
+      which, when used for archiving, effectively halts WAL archival, but if
+      <xref linkend="guc-archive-mode"/> is enabled, the server will accumulate
+      WAL segment files in the expectation that a value will soon be provided.
+      When an empty string is used for recovery, restore will fail.
      </para>
     </listitem>
    </varlistentry>
@@ -46,7 +50,7 @@
 
   <para>
    These parameters must be set in <filename>postgresql.conf</filename>.
-   Typical usage might be:
+   Typical usage as an archive module might be:
   </para>
 
 <programlisting>
@@ -61,6 +65,7 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
+   When <filename>basic_archive</filename> is used as an archive module,
    Server crashes may leave temporary files with the prefix
    <filename>archtemp</filename> in the archive directory.  It is recommended to
    delete such files before restarting the server after a crash.  It is safe to
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 65a6e6c408..9a2570f87f 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3906,7 +3906,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
@@ -3934,7 +3935,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,
@@ -3969,7 +3971,42 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
 
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>restore_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-restore-library" xreflabel="restore_library">
+      <term><varname>restore_library</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>restore_library</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        The library to use for restore actions, including retrieving archived
+        files and executing tasks at restartpoints and at recovery end.  Either
+        <xref linkend="guc-restore-command"/> or
+        <varname>restore_library</varname> is required for archive recovery,
+        but optional for streaming replication.  If this parameter is set to an
+        empty string (the default), restoring via shell is enabled, and
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, and
+        <varname>recovery_end_command</varname> are used.  If both
+        <varname>restore_library</varname> and any of
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, or
+        <varname>recovery_end_command</varname> are set, an error will be
+        raised.  Otherwise, the specified shared library is used for restoring.
+        For more information, see <xref linkend="restore-modules"/>.
+       </para>
+
+       <para>
+        This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
        </para>
       </listitem>
      </varlistentry>
@@ -4014,7 +4051,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>
@@ -4043,7 +4083,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
        </para>
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>recovery_end_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index b48209fc2f..a86a9fe93a 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -627,7 +627,8 @@ protocol to make nodes agree on a serializable transactional order.
    <para>
     In standby mode, the server continuously applies WAL received from the
     primary server. The standby server can read WAL from a WAL archive
-    (see <xref linkend="guc-restore-command"/>) or directly from the primary
+    (see <xref linkend="guc-restore-command"/> and
+    <xref linkend="guc-restore-library"/>) or directly from the primary
     over a TCP connection (streaming replication). The standby server will
     also attempt to restore any WAL found in the standby cluster's
     <filename>pg_wal</filename> directory. That typically happens after a server
@@ -638,8 +639,10 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     At startup, the standby begins by restoring all WAL available in the
-    archive location, calling <varname>restore_command</varname>. Once it
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s callbacks.  Once it
     reaches the end of WAL available there and <varname>restore_command</varname>
+    or <varname>restore_library</varname>
     fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
     If that fails, and streaming replication has been configured, the
     standby tries to connect to the primary server and start streaming WAL
@@ -698,7 +701,8 @@ protocol to make nodes agree on a serializable transactional order.
     server (see <xref linkend="backup-pitr-recovery"/>). Create a file
     <link linkend="file-standby-signal"><filename>standby.signal</filename></link><indexterm><primary>standby.signal</primary></indexterm>
     in the standby's cluster data
-    directory. Set <xref linkend="guc-restore-command"/> to a simple command to copy files from
+    directory. Set <xref linkend="guc-restore-command"/> or
+    <xref linkend="guc-restore-library"/> to copy files from
     the WAL archive. If you plan to have multiple standby servers for high
     availability purposes, make sure that <varname>recovery_target_timeline</varname> is set to
     <literal>latest</literal> (the default), to make the standby server follow the timeline change
@@ -707,7 +711,8 @@ protocol to make nodes agree on a serializable transactional order.
 
    <note>
      <para>
-     <xref linkend="guc-restore-command"/> should return immediately
+     <xref linkend="guc-restore-command"/> and
+     <xref linkend="guc-restore-library"/> should return immediately
      if the file does not exist; the server will retry the command again if
      necessary.
     </para>
@@ -731,8 +736,9 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     If you're using a WAL archive, its size can be minimized using the <xref
-    linkend="guc-archive-cleanup-command"/> parameter to remove files that are no
-    longer required by the standby server.
+    linkend="guc-archive-cleanup-command"/> parameter or the
+    <xref linkend="guc-restore-library"/>'s archive cleanup callbacks to remove
+    files that are no longer required by the standby server.
     The <application>pg_archivecleanup</application> utility is designed specifically to
     be used with <varname>archive_cleanup_command</varname> in typical single-standby
     configurations, see <xref linkend="pgarchivecleanup"/>.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index e5d12f1d15..34a21c40a5 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -83,7 +83,7 @@
 #include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
-#include "restore/shell_restore.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
@@ -5178,18 +5178,19 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during
+	 * The callback is provided the archive file cutoff point for use during
 	 * log shipping replication.  All files earlier than this point can be
 	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_recovery_end_configured())
+	if (recovery_end_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_recovery_end(lastRestartPointFname);
+		RestoreCallbacks->recovery_end_cb(restore_module_state,
+										  lastRestartPointFname);
 	}
 
 	/*
@@ -7673,18 +7674,19 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Finally, execute archive-cleanup callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during
+	 * The callback is provided the archive file cutoff point for use during
 	 * log shipping replication.  All files earlier than this point can be
 	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_archive_cleanup_configured())
+	if (archive_cleanup_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_archive_cleanup(lastRestartPointFname);
+		RestoreCallbacks->archive_cleanup_cb(restore_module_state,
+											 lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index 5756f52124..3a05581495 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -23,14 +23,21 @@
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
 #include "common/archive.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/pgarch.h"
 #include "postmaster/startup.h"
 #include "replication/walsender.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
+#include "utils/memutils.h"
+
+char	   *restoreLibrary = "";
+const RestoreModuleCallbacks *RestoreCallbacks = NULL;
+RestoreModuleState *restore_module_state;
 
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
@@ -74,15 +81,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			if (!shell_restore_file_configured())
+			if (!restore_wal_segment_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			if (!shell_restore_file_configured())
+			if (!restore_timeline_history_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			if (!shell_restore_file_configured())
+			if (!timeline_history_exists_configured())
 				goto not_available;
 			break;
 	}
@@ -174,14 +181,17 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			ret = shell_restore_wal_segment(xlogfname, xlogpath,
-											lastRestartPointFname);
+			ret = RestoreCallbacks->restore_wal_segment_cb(restore_module_state,
+														   xlogfname, xlogpath,
+														   lastRestartPointFname);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->restore_timeline_history_cb(restore_module_state,
+																xlogfname, xlogpath);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->timeline_history_exists_cb(restore_module_state,
+															   xlogfname, xlogpath);
 			break;
 	}
 
@@ -189,6 +199,16 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 
 	if (ret)
 	{
+		/*
+		 * Some restore functions might not copy the file (e.g., checking
+		 * whether a timeline history file exists), but they can set a flag to
+		 * tell us if they do.  We only need to verify file existence if this
+		 * flag is enabled.
+		 */
+		if (archive_type == ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS &&
+			!restore_module_state->timeline_history_exists_cb_copies)
+			return true;
+
 		/*
 		 * command apparently succeeded, but let's make sure the file is
 		 * really there now and has the correct size.
@@ -630,3 +650,65 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the restore callbacks into our global RestoreCallbacks.  The
+ * caller is responsible for validating the combination of library/command
+ * parameters that are set (e.g., restore_command and restore_library cannot
+ * both be set).
+ */
+void
+LoadRestoreCallbacks(void)
+{
+	RestoreModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_restore_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RestoreModuleInit)
+			load_external_function(restoreLibrary, "_PG_restore_module_init",
+								   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("restore modules have to define the symbol "
+						"_PG_restore_module_init")));
+
+	RestoreCallbacks = (*init) ();
+
+	/* restore state should be freed before calling this function */
+	Assert(restore_module_state == NULL);
+	restore_module_state = (RestoreModuleState *)
+		MemoryContextAllocZero(TopMemoryContext,
+							   sizeof(RestoreModuleState));
+
+	if (RestoreCallbacks->startup_cb != NULL)
+		RestoreCallbacks->startup_cb(restore_module_state);
+}
+
+/*
+ * Call the shutdown callback of the loaded restore module, if defined.  Also,
+ * free the restore module state if it was allocated.
+ *
+ * Processes that load restore libraries should register this via
+ * before_shmem_exit().
+ */
+void
+call_restore_module_shutdown_cb(int code, Datum arg)
+{
+	if (RestoreCallbacks != NULL &&
+		RestoreCallbacks->shutdown_cb != NULL &&
+		restore_module_state != NULL)
+		RestoreCallbacks->shutdown_cb(restore_module_state);
+
+	if (restore_module_state != NULL)
+	{
+		pfree(restore_module_state);
+		restore_module_state = NULL;
+	}
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 07213b1897..1bf0d0ea11 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -51,6 +51,7 @@
 #include "replication/slot.h"
 #include "replication/slotsync.h"
 #include "replication/walreceiver.h"
+#include "restore/restore_module.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1117,18 +1118,21 @@ validateRecoveryParameters(void)
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			(!restore_wal_segment_configured() ||
+			 !restore_timeline_history_configured() ||
+			 !timeline_history_exists_configured()))
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
+					(errmsg("specified neither primary_conninfo nor restore_command nor a configured restore_library"),
 					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
 	}
 	else
 	{
-		if (recoveryRestoreCommand == NULL ||
-			strcmp(recoveryRestoreCommand, "") == 0)
+		if (!restore_wal_segment_configured() ||
+			!restore_timeline_history_configured() ||
+			!timeline_history_exists_configured())
 			ereport(FATAL,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("must specify restore_command when standby mode is not enabled")));
+					 errmsg("must specify restore_command or a configured restore_library when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 46197d56f8..47993aa35e 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -45,6 +45,7 @@
 #include "postmaster/bgwriter.h"
 #include "postmaster/interrupt.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -224,6 +225,25 @@ CheckpointerMain(void)
 												 ALLOCSET_DEFAULT_SIZES);
 	MemoryContextSwitchTo(checkpointer_context);
 
+	/*
+	 * Load restore_library so that we can use its archive-cleanup callback,
+	 * if one is defined.
+	 *
+	 * We also take this opportunity to set up the before_shmem_exit hook for
+	 * the shutdown callback and to check that only one of restore_library,
+	 * archive_cleanup_command is set.  Note that we emit a WARNING upon
+	 * detecting misconfigured parameters, and we clear the callbacks so that
+	 * the archive-cleanup functionality is skipped.  We judge this
+	 * functionality to not be important enough to require blocking
+	 * checkpoints or shutting down the server when the parameters are
+	 * misconfigured.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+										 archiveCleanupCommand, "archive_cleanup_command",
+										 WARNING))
+		LoadRestoreCallbacks();
+
 	/*
 	 * If an exception is encountered, processing resumes here.
 	 *
@@ -556,6 +576,10 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		bool		archive_cleanup_settings_changed = false;
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -571,6 +595,36 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		/*
+		 * If the archive-cleanup settings have changed, call the currently
+		 * loaded shutdown callback and clear all the restore callbacks.
+		 * There's presently no good way to unload a library besides
+		 * restarting the process, and there should be little harm in leaving
+		 * it around, so we just leave it loaded.
+		 */
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			archive_cleanup_settings_changed = true;
+			call_restore_module_shutdown_cb(0, (Datum) 0);
+			RestoreCallbacks = NULL;
+		}
+
+		/*
+		 * As in CheckpointerMain(), we only emit a WARNING if we detect that
+		 * both restore_library and archive_cleanup_command are set.  We do
+		 * this even if the archive-cleanup settings haven't changed to remind
+		 * the user about the misconfiguration.
+		 */
+		if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											 archiveCleanupCommand, "archive_cleanup_command",
+											 WARNING) &&
+			archive_cleanup_settings_changed)
+			LoadRestoreCallbacks();
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index e391bf6aa6..3cbd382e00 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -25,6 +25,7 @@
 #include "libpq/pqsignal.h"
 #include "miscadmin.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "storage/ipc.h"
 #include "storage/pmsignal.h"
 #include "storage/procsignal.h"
@@ -118,13 +119,17 @@ StartupProcShutdownHandler(SIGNAL_ARGS)
  * Re-read the config file.
  *
  * If one of the critical walreceiver options has changed, flag xlog.c
- * to restart it.
+ * to restart it.  Also, check for invalid combinations of the command/library
+ * parameters and reload the restore callbacks if necessary.
  */
 static void
 StartupRereadConfig(void)
 {
 	char	   *conninfo = pstrdup(PrimaryConnInfo);
 	char	   *slotname = pstrdup(PrimarySlotName);
+	char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+	char	   *prevRestoreCommand = pstrdup(recoveryRestoreCommand);
+	char	   *prevRecoveryEndCommand = pstrdup(recoveryEndCommand);
 	bool		tempSlot = wal_receiver_create_temp_slot;
 	bool		conninfoChanged;
 	bool		slotnameChanged;
@@ -146,6 +151,30 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	/*
+	 * If the restore settings have changed, call the currently loaded
+	 * shutdown callback and load the new callbacks.  There's presently no
+	 * good way to unload a library besides restarting the process, and there
+	 * should be little harm in leaving it around, so we just leave it loaded.
+	 */
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_restore_module_shutdown_cb(0, (Datum) 0);
+		LoadRestoreCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
@@ -255,6 +284,19 @@ StartupProcessMain(void)
 	 */
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	LoadRestoreCallbacks();
+
 	/*
 	 * Do what we came for.
 	 */
diff --git a/src/backend/restore/shell_restore.c b/src/backend/restore/shell_restore.c
index 42291625c1..be25f04044 100644
--- a/src/backend/restore/shell_restore.c
+++ b/src/backend/restore/shell_restore.c
@@ -3,7 +3,8 @@
  * shell_restore.c
  *
  * These restore functions use a user-specified shell command (e.g., the
- * restore_command GUC).
+ * restore_command GUC).  It is used as the default, but other modules may
+ * define their own restore logic.
  *
  * Copyright (c) 2024, PostgreSQL Global Development Group
  *
@@ -22,25 +23,76 @@
 #include "common/archive.h"
 #include "common/percentrepl.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static void shell_restore_startup(RestoreModuleState *state);
+static bool shell_restore_file_configured(RestoreModuleState *state);
 static bool shell_restore_file(const char *file, const char *path,
 							   const char *lastRestartPointFileName);
+static bool shell_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool shell_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool shell_archive_cleanup_configured(RestoreModuleState *state);
+static void shell_archive_cleanup(RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+static bool shell_recovery_end_configured(RestoreModuleState *state);
+static void shell_recovery_end(RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
 static void ExecuteRecoveryCommand(const char *command,
 								   const char *commandName,
 								   bool failOnSignal,
 								   uint32 wait_event_info,
 								   const char *lastRestartPointFileName);
 
+static const RestoreModuleCallbacks shell_restore_callbacks = {
+	.startup_cb = shell_restore_startup,
+	.restore_wal_segment_configured_cb = shell_restore_file_configured,
+	.restore_wal_segment_cb = shell_restore_wal_segment,
+	.restore_timeline_history_configured_cb = shell_restore_file_configured,
+	.restore_timeline_history_cb = shell_restore_timeline_history,
+	.timeline_history_exists_configured_cb = shell_restore_file_configured,
+	.timeline_history_exists_cb = shell_restore_timeline_history,
+	.archive_cleanup_configured_cb = shell_archive_cleanup_configured,
+	.archive_cleanup_cb = shell_archive_cleanup,
+	.recovery_end_configured_cb = shell_recovery_end_configured,
+	.recovery_end_cb = shell_recovery_end,
+	.shutdown_cb = NULL
+};
+
+/*
+ * shell_restore_init
+ *
+ * Returns the callbacks for restoring via shell.
+ */
+const RestoreModuleCallbacks *
+shell_restore_init(void)
+{
+	return &shell_restore_callbacks;
+}
+
+/*
+ * Indicates that our timeline_history_exists_cb only attempts to retrieve the
+ * file, so the caller should verify the file exists afterwards.
+ */
+static void
+shell_restore_startup(RestoreModuleState *state)
+{
+	state->timeline_history_exists_cb_copies = true;
+}
+
 /*
  * Attempt to execute a shell-based restore command to retrieve a WAL segment.
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_wal_segment(const char *file, const char *path,
+static bool
+shell_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
 						  const char *lastRestartPointFileName)
 {
 	return shell_restore_file(file, path, lastRestartPointFileName);
@@ -52,8 +104,9 @@ shell_restore_wal_segment(const char *file, const char *path,
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_timeline_history(const char *file, const char *path)
+static bool
+shell_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
 {
 	char		lastRestartPointFname[MAXPGPATH];
 
@@ -64,8 +117,8 @@ shell_restore_timeline_history(const char *file, const char *path)
 /*
  * Check whether restore_command is supplied.
  */
-bool
-shell_restore_file_configured(void)
+static bool
+shell_restore_file_configured(RestoreModuleState *state)
 {
 	return recoveryRestoreCommand[0] != '\0';
 }
@@ -152,8 +205,8 @@ shell_restore_file(const char *file, const char *path,
 /*
  * Check whether archive_cleanup_command is supplied.
  */
-bool
-shell_archive_cleanup_configured(void)
+static bool
+shell_archive_cleanup_configured(RestoreModuleState *state)
 {
 	return archiveCleanupCommand[0] != '\0';
 }
@@ -161,8 +214,9 @@ shell_archive_cleanup_configured(void)
 /*
  * Attempt to execute a shell-based archive cleanup command.
  */
-void
-shell_archive_cleanup(const char *lastRestartPointFileName)
+static void
+shell_archive_cleanup(RestoreModuleState *state,
+					  const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(archiveCleanupCommand, "archive_cleanup_command",
 						   false, WAIT_EVENT_ARCHIVE_CLEANUP_COMMAND,
@@ -172,8 +226,8 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 /*
  * Check whether recovery_end_command is supplied.
  */
-bool
-shell_recovery_end_configured(void)
+static bool
+shell_recovery_end_configured(RestoreModuleState *state)
 {
 	return recoveryEndCommand[0] != '\0';
 }
@@ -181,8 +235,9 @@ shell_recovery_end_configured(void)
 /*
  * Attempt to execute a shell-based end-of-recovery command.
  */
-void
-shell_recovery_end(const char *lastRestartPointFileName)
+static void
+shell_recovery_end(RestoreModuleState *state,
+				   const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(recoveryEndCommand, "recovery_end_command", true,
 						   WAIT_EVENT_RECOVERY_END_COMMAND,
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 57d9de4dd9..295cde9247 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -70,6 +70,7 @@
 #include "replication/slot.h"
 #include "replication/slotsync.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -3946,6 +3947,16 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"restore_library", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
+			gettext_noop("Sets the library that will be called for restore actions."),
+			NULL
+		},
+		&restoreLibrary,
+		"",
+		NULL, NULL, NULL
+	},
+
 	{
 		{"archive_cleanup_command", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
 			gettext_noop("Sets the shell command that will be executed at every restart point."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 2244ee52f7..5265857519 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -284,6 +284,7 @@
 				# placeholders: %p = path of file to restore
 				#               %f = file name only
 				# e.g. 'cp /mnt/server/archivedir/%f %p'
+#restore_library = ''		# library to use for restore actions
 #archive_cleanup_command = ''	# command to execute at every restartpoint
 #recovery_end_command = ''	# command to execute at completion of recovery
 
diff --git a/src/include/restore/restore_module.h b/src/include/restore/restore_module.h
new file mode 100644
index 0000000000..cc148e855a
--- /dev/null
+++ b/src/include/restore/restore_module.h
@@ -0,0 +1,143 @@
+/*-------------------------------------------------------------------------
+ *
+ * restore_module.h
+ *		Exports for restore modules.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * src/include/restore/restore_module.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef _RESTORE_MODULE_H
+#define _RESTORE_MODULE_H
+
+/*
+ * The value of the restore_library GUC.
+ */
+extern PGDLLIMPORT char *restoreLibrary;
+
+typedef struct RestoreModuleState
+{
+	/*
+	 * Private data pointer for use by a restore module.  This can be used to
+	 * store state for the module that will be passed to each of its
+	 * callbacks.
+	 */
+	void	   *private_data;
+
+	/*
+	 * Indicates whether the callback that checks for timeline existence
+	 * merely copies the file.  If set to true, the server will verify that
+	 * the timeline history file exists after the callback returns.
+	 */
+	bool		timeline_history_exists_cb_copies;
+} RestoreModuleState;
+
+/*
+ * Restore module callbacks
+ *
+ * These callback functions should be defined by restore libraries and returned
+ * via _PG_restore_module_init().  For more information about the purpose of
+ * each callback, refer to the restore modules documentation.
+ */
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state,
+									 const char *file, const char *path,
+									 const char *lastRestartPointFileName);
+typedef bool (*RestoreTimelineHistoryConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state,
+										  const char *file, const char *path);
+typedef bool (*TimelineHistoryExistsConfiguredCB) (RestoreModuleState *state);
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state,
+										 const char *file, const char *path);
+typedef bool (*ArchiveCleanupConfiguredCB) (RestoreModuleState *state);
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+typedef bool (*RecoveryEndConfiguredCB) (RestoreModuleState *state);
+typedef void (*RecoveryEndCB) (RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+
+typedef struct RestoreModuleCallbacks
+{
+	RestoreStartupCB startup_cb;
+	RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+	RestoreWalSegmentCB restore_wal_segment_cb;
+	RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+	RestoreTimelineHistoryCB restore_timeline_history_cb;
+	TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+	TimelineHistoryExistsCB timeline_history_exists_cb;
+	ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+	ArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndConfiguredCB recovery_end_configured_cb;
+	RecoveryEndCB recovery_end_cb;
+	RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+
+/*
+ * Type of the shared library symbol _PG_restore_module_init that is looked up
+ * when loading a restore library.
+ */
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+
+extern PGDLLEXPORT const RestoreModuleCallbacks *_PG_restore_module_init(void);
+
+extern void LoadRestoreCallbacks(void);
+extern void call_restore_module_shutdown_cb(int code, Datum arg);
+
+extern const RestoreModuleCallbacks *RestoreCallbacks;
+extern RestoreModuleState *restore_module_state;
+
+/*
+ * The following *_configured() functions are convenient wrappers around the
+ * *_configured_cb() callback functions with additional defensive NULL checks.
+ */
+
+static inline bool
+restore_wal_segment_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->restore_wal_segment_configured_cb != NULL &&
+		RestoreCallbacks->restore_wal_segment_cb != NULL &&
+		RestoreCallbacks->restore_wal_segment_configured_cb(restore_module_state);
+}
+
+static inline bool
+restore_timeline_history_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->restore_timeline_history_configured_cb != NULL &&
+		RestoreCallbacks->restore_timeline_history_cb != NULL &&
+		RestoreCallbacks->restore_timeline_history_configured_cb(restore_module_state);
+}
+
+static inline bool
+timeline_history_exists_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->timeline_history_exists_configured_cb != NULL &&
+		RestoreCallbacks->timeline_history_exists_cb != NULL &&
+		RestoreCallbacks->timeline_history_exists_configured_cb(restore_module_state);
+}
+
+static inline bool
+archive_cleanup_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->archive_cleanup_configured_cb != NULL &&
+		RestoreCallbacks->archive_cleanup_cb != NULL &&
+		RestoreCallbacks->archive_cleanup_configured_cb(restore_module_state);
+}
+
+static inline bool
+recovery_end_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->recovery_end_configured_cb != NULL &&
+		RestoreCallbacks->recovery_end_cb != NULL &&
+		RestoreCallbacks->recovery_end_configured_cb(restore_module_state);
+}
+
+#endif							/* _RESTORE_MODULE_H */
diff --git a/src/include/restore/shell_restore.h b/src/include/restore/shell_restore.h
index 28b5b7bc92..6352623866 100644
--- a/src/include/restore/shell_restore.h
+++ b/src/include/restore/shell_restore.h
@@ -12,15 +12,13 @@
 #ifndef _SHELL_RESTORE_H
 #define _SHELL_RESTORE_H
 
-extern bool shell_restore_file_configured(void);
-extern bool shell_restore_wal_segment(const char *file, const char *path,
-									  const char *lastRestartPointFileName);
-extern bool shell_restore_timeline_history(const char *file, const char *path);
+#include "restore/restore_module.h"
 
-extern bool shell_archive_cleanup_configured(void);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-
-extern bool shell_recovery_end_configured(void);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Since the logic for restoring via shell commands is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern const RestoreModuleCallbacks *shell_restore_init(void);
 
 #endif							/* _SHELL_RESTORE_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 2f975ac53a..8e190a0c79 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2388,6 +2388,8 @@ ResourceOwnerDesc
 ResourceReleaseCallback
 ResourceReleaseCallbackItem
 ResourceReleasePhase
+RestoreModuleCallbacks
+RestoreModuleState
 RestoreOptions
 RestorePass
 RestrictInfo
-- 
2.25.1


--oyUTqETQ0mS9luUI--





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

* [PATCH v19 5/5] introduce restore_library
@ 2023-02-16 06:06  Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 28+ messages in thread

From: Nathan Bossart @ 2023-02-16 06:06 UTC (permalink / raw)

---
 contrib/basic_archive/Makefile                |   2 +
 contrib/basic_archive/basic_archive.c         | 153 +++++++-
 contrib/basic_archive/meson.build             |   5 +
 contrib/basic_archive/t/001_restore.pl        |  44 +++
 doc/src/sgml/archive-and-restore-modules.sgml | 356 +++++++++++++++++-
 doc/src/sgml/backup.sgml                      |  40 +-
 doc/src/sgml/basic-archive.sgml               |  31 +-
 doc/src/sgml/config.sgml                      |  53 ++-
 doc/src/sgml/high-availability.sgml           |  18 +-
 src/backend/access/transam/xlog.c             |  20 +-
 src/backend/access/transam/xlogarchive.c      |  96 ++++-
 src/backend/access/transam/xlogrecovery.c     |  14 +-
 src/backend/postmaster/checkpointer.c         |  54 +++
 src/backend/postmaster/startup.c              |  44 ++-
 src/backend/restore/shell_restore.c           |  85 ++++-
 src/backend/utils/misc/guc_tables.c           |  11 +
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/restore/restore_module.h          | 143 +++++++
 src/include/restore/shell_restore.h           |  16 +-
 src/tools/pgindent/typedefs.list              |   2 +
 20 files changed, 1104 insertions(+), 84 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl
 create mode 100644 src/include/restore/restore_module.h

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 100ed81f12..e61f7d676f 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -10,6 +10,8 @@ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.c
 # typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
 
+TAP_TESTS = 1
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index 6b102e9072..e0d8dce364 100644
--- a/contrib/basic_archive/basic_archive.c
+++ b/contrib/basic_archive/basic_archive.c
@@ -17,6 +17,11 @@
  * a file is successfully archived and then the system crashes before
  * a durable record of the success has been made.
  *
+ * This file also demonstrates a basic restore library implementation that
+ * is roughly equivalent to the following shell command:
+ *
+ *		cp /path/to/archivedir/%f %p
+ *
  * Copyright (c) 2022-2024, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
@@ -33,6 +38,7 @@
 #include "archive/archive_module.h"
 #include "common/int.h"
 #include "miscadmin.h"
+#include "restore/restore_module.h"
 #include "storage/copydir.h"
 #include "storage/fd.h"
 #include "utils/guc.h"
@@ -54,6 +60,16 @@ static void basic_archive_file_internal(const char *file, const char *path);
 static bool check_archive_directory(char **newval, void **extra, GucSource source);
 static bool compare_files(const char *file1, const char *file2);
 static void basic_archive_shutdown(ArchiveModuleState *state);
+static bool basic_restore_configured(RestoreModuleState *state);
+static bool basic_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool basic_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool basic_restore_file(const char *file, const char *path);
+static bool basic_restore_timeline_history_exists(RestoreModuleState *state,
+												  const char *file,
+												  const char *path);
 
 static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.startup_cb = basic_archive_startup,
@@ -62,6 +78,21 @@ static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.shutdown_cb = basic_archive_shutdown
 };
 
+static const RestoreModuleCallbacks basic_restore_callbacks = {
+	.startup_cb = NULL,
+	.restore_wal_segment_configured_cb = basic_restore_configured,
+	.restore_wal_segment_cb = basic_restore_wal_segment,
+	.restore_timeline_history_configured_cb = basic_restore_configured,
+	.restore_timeline_history_cb = basic_restore_timeline_history,
+	.timeline_history_exists_configured_cb = basic_restore_configured,
+	.timeline_history_exists_cb = basic_restore_timeline_history_exists,
+	.archive_cleanup_configured_cb = NULL,
+	.archive_cleanup_cb = NULL,
+	.recovery_end_configured_cb = NULL,
+	.recovery_end_cb = NULL,
+	.shutdown_cb = NULL
+};
+
 /*
  * _PG_init
  *
@@ -71,7 +102,7 @@ void
 _PG_init(void)
 {
 	DefineCustomStringVariable("basic_archive.archive_directory",
-							   gettext_noop("Archive file destination directory."),
+							   gettext_noop("Archive file source/destination directory."),
 							   NULL,
 							   &archive_directory,
 							   "",
@@ -93,6 +124,17 @@ _PG_archive_module_init(void)
 	return &basic_archive_callbacks;
 }
 
+/*
+ * _PG_restore_module_init
+ *
+ * Returns the module's restore callbacks.
+ */
+const RestoreModuleCallbacks *
+_PG_restore_module_init(void)
+{
+	return &basic_restore_callbacks;
+}
+
 /*
  * basic_archive_startup
  *
@@ -431,3 +473,112 @@ basic_archive_shutdown(ArchiveModuleState *state)
 	pfree(data);
 	state->private_data = NULL;
 }
+
+/*
+ * basic_restore_configured
+ *
+ * Checks that archive_directory is not blank.
+ */
+static bool
+basic_restore_configured(RestoreModuleState *state)
+{
+	return archive_directory != NULL && archive_directory[0] != '\0';
+}
+
+/*
+ * basic_restore_wal_segment
+ *
+ * Retrieves one archived WAL segment.
+ */
+static bool
+basic_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
+						  const char *lastRestartPointFileName)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_timeline_history
+ *
+ * Retrieves one timeline history file.
+ */
+static bool
+basic_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file.
+ */
+static bool
+basic_restore_file(const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive",
+					file)));
+
+	/*
+	 * If the file doesn't exist, return false to indicate that there are no
+	 * more files to restore.
+	 */
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	copy_file(archive_path, path);
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive",
+					file)));
+	return true;
+}
+
+/*
+ * basic_restore_timeline_history_exists
+ *
+ * Check whether a timeline history file is present in the archive directory.
+ */
+static bool
+basic_restore_timeline_history_exists(RestoreModuleState *state,
+									  const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("checking existence of \"%s\" via basic_archive",
+					file)));
+
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	ereport(DEBUG1,
+			(errmsg("verified existence of \"%s\" via basic_archive",
+					file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index cc2f62bf36..8e36fa7ed6 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -31,4 +31,9 @@ tests += {
     # typical installcheck users do not have (e.g. buildfarm clients).
     'runningcheck': false,
   },
+  'tap': {
+    'tests': [
+      't/001_restore.pl',
+    ],
+  },
 }
diff --git a/contrib/basic_archive/t/001_restore.pl b/contrib/basic_archive/t/001_restore.pl
new file mode 100644
index 0000000000..6c01f736cf
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,44 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# start a node
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init(has_archiving => 1, allows_streaming => 1);
+my $archive_dir = $node->archive_dir;
+$archive_dir =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
+$node->append_conf('postgresql.conf', "archive_command = ''");
+$node->append_conf('postgresql.conf', "archive_library = 'basic_archive'");
+$node->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$node->start;
+
+# backup the node
+my $backup = 'backup';
+$node->backup($backup);
+
+# generate some new WAL files
+$node->safe_psql('postgres', "CREATE TABLE test (a INT);");
+$node->safe_psql('postgres', "SELECT pg_switch_wal();");
+$node->safe_psql('postgres', "INSERT INTO test VALUES (1);");
+
+# shut down the node (this should archive all WAL files)
+$node->stop;
+
+# restore from the backup
+my $restore = PostgreSQL::Test::Cluster->new('restore');
+$restore->init_from_backup($node, $backup, has_restoring => 1, standby => 0);
+$restore->append_conf('postgresql.conf', "restore_command = ''");
+$restore->append_conf('postgresql.conf', "restore_library = 'basic_archive'");
+$restore->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$restore->start;
+
+# ensure post-backup WAL is replayed
+my $result = $restore->poll_query_until("postgres", "SELECT count(*) = 1 FROM test;");
+is($result, "1", "check restore content");
+
+done_testing();
diff --git a/doc/src/sgml/archive-and-restore-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml
index a52d15082d..15be548a73 100644
--- a/doc/src/sgml/archive-and-restore-modules.sgml
+++ b/doc/src/sgml/archive-and-restore-modules.sgml
@@ -8,15 +8,17 @@
 
  <para>
   PostgreSQL provides infrastructure to create custom modules for continuous
-  archiving (see <xref linkend="continuous-archiving"/>).  While archiving via
-  a shell command (i.e., <xref linkend="guc-archive-command"/>) is much
-  simpler, a custom archive module will often be considerably more robust and
-  performant.
+  archiving and recovery (see <xref linkend="continuous-archiving"/>).  While a
+  shell command (i.e., <xref linkend="guc-archive-command"/>,
+  <xref linkend="guc-restore-command"/>) is much simpler, a custom module will
+  often be considerably more robust and performant.
  </para>
 
  <para>
   The <filename>contrib/basic_archive</filename> module contains a working
-  example, which demonstrates some useful techniques.
+  example, which demonstrates some useful techniques.  As demonstrated in
+  <filename>basic_archive</filename> a single module may serve as both an
+  archive module and a restore module.
  </para>
 
  <sect1 id="archive-modules">
@@ -178,4 +180,348 @@ typedef void (*ArchiveShutdownCB) (ArchiveModuleState *state);
    </sect3>
   </sect2>
  </sect1>
+
+ <sect1 id="restore-modules">
+  <title>Restore Modules</title>
+  <indexterm zone="restore-modules">
+   <primary>Restore Modules</primary>
+  </indexterm>
+
+  <para>
+   When a custom <xref linkend="guc-restore-library"/> is configured,
+   PostgreSQL will use the module for restore actions.  It is
+   ultimately up to the module to decide how to accomplish each task,
+   but some recommendations are listed at
+   <xref linkend="backup-pitr-recovery"/>.
+  </para>
+
+  <para>
+   Restore modules must at least consist of an initialization function
+   (see <xref linkend="restore-module-init"/>) and the required callbacks (see
+   <xref linkend="restore-module-callbacks"/>).  However, restore modules are
+   also permitted to do much more (e.g., declare GUCs and register background
+   workers).
+  </para>
+
+  <sect2 id="restore-module-init">
+   <title>Initialization Functions</title>
+   <indexterm zone="restore-module-init">
+    <primary>_PG_restore_module_init</primary>
+   </indexterm>
+
+   <para>
+    An restore library is loaded by dynamically loading a shared library with
+    the <xref linkend="guc-restore-library"/>'s name as the library base name.
+    The normal library search path is used to locate the library.  To provide
+    the required restore module callbacks and to indicate that the library is
+    actually a restore module, it needs to provide a function named
+    <function>_PG_restore_module_init</function>.  The result of the function
+    must be a pointer to a struct of type
+    <structname>RestoreModuleCallbacks</structname>, which contains everything
+    that the core code needs to know how to make use of the restore module.
+    The return value needs to be of server lifetime, which is typically
+    achieved by defining it as a <literal>static const</literal> variable in
+    global scope.
+
+<programlisting>
+typedef struct RestoreModuleCallbacks
+{
+    RestoreStartupCB startup_cb;
+    RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+    RestoreWalSegmentCB restore_wal_segment_cb;
+    RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+    RestoreTimelineHistoryCB restore_timeline_history_cb;
+    TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+    TimelineHistoryExistsCB timeline_history_exists_cb;
+    ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+    ArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndConfiguredCB recovery_end_configured_cb;
+    RecoveryEndCB recovery_end_cb;
+    RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+</programlisting>
+
+    The <function>restore_wal_segment_configured_cb</function>,
+    <function>restore_wal_segment_cb</function>,
+    <function>restore_timeline_history_configured_cb</function>,
+    <function>restore_timeline_history_cb</function>,
+    <function>timeline_history_exists_configured_cb</function>, and
+    <function>timeline_history_exists_cb</function> callbacks are required for
+    archive recovery but optional for streaming replication.  The others are
+    always optional.
+   </para>
+  </sect2>
+
+  <sect2 id="restore-module-callbacks">
+   <title>Restore Module Callbacks</title>
+
+   <para>
+    The restore callbacks define the actual behavior of the module.  The server
+    will call them as required to execute restore actions.
+   </para>
+
+   <sect3 id="restore-module-startup">
+    <title>Startup Callback</title>
+
+    <para>
+     The <function>startup_cb</function> callback is called shortly after the
+     module is loaded.  This callback can be used to perform any additional
+     initialization required.  If the restore module has state, it can use
+     <structfield>state->private_data</structfield> to store it.
+
+<programlisting>
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+</programlisting>
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment-configured">
+    <title>Restore WAL Segment Configured Callback</title>
+    <para>
+     The <function>restore_wal_segment_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to retrieve WAL files (e.g., its configuration parameters
+     are set to valid values).  If no
+     <function>restore_wal_segment_configured_cb</function> is defined, the
+     server always assumes the module is not configured to retrieve WAL files.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve WAL files
+     by calling the <function>restore_wal_segment_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_wal_segment_cb</function> callback to retrieve WAL
+     files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment">
+    <title>Restore WAL Segment Callback</title>
+    <para>
+     The <function>restore_wal_segment_cb</function> callback is called to
+     retrieve a single archived segment of the WAL file series for archive
+     recovery or streaming replication.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state, const char *file, const char *path, const char *lastRestartPointFileName);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point.  That is the earliest
+     file that must be kept to allow a restore to be restartable, so this
+     information can be used to truncate the archive to just the minimum
+     required to support restarting from the current restore.
+     <replaceable>lastRestartPointFileName</replaceable> is typically only used
+     by warm-standby configurations (see <xref linkend="warm-standby"/>).  Note
+     that if multiple standby servers are restoring from the same archive
+     directory, you will need to ensure that you do not delete WAL files until
+     they are no longer needed by any of the servers.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history-configured">
+    <title>Restore Timeline History Configured Callback</title>
+    <para>
+     The <function>restore_timeline_history_configured_cb</function> callback
+     is called to determine whether the module is fully configured and ready to
+     accept requests to retrieve timeline history files (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>restore_timeline_history_configured_cb</function> is defined,
+     the server always assumes the module is not configured to retrieve
+     timeline history files.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve timeline
+     history files by calling the
+     <function>restore_timeline_history_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_timeline_history_cb</function> callback to retrieve
+     timeline history files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history">
+    <title>Restore Timeline History Callback</title>
+    <para>
+     The <function>restore_timeline_history_cb</function> callback is called to
+     retrieve a single archived timeline history file for archive recovery or
+     streaming replication.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists-configured">
+    <title>Timeline History Exists Configured Callback</title>
+    <para>
+     The <function>timeline_history_exists_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to determine whether a timeline history file exists in the
+     archives (e.g., its configuration parameters are set to valid values).  If
+     no <function>timeline_history_exists_configured_cb</function> is defined,
+     the server always assumes the module is not configured to check whether
+     certain timeline history files exist.
+
+<programlisting>
+typedef bool (*TimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will check whether
+     certain timeline history files are present in the archives by calling the
+     <function>timeline_history_exists_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>timeline_history_exists_cb</function> callback to check if
+     timeline history files exist in the archives.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists">
+    <title>Timeline History Exists Callback</title>
+    <para>
+     The <function>timeline_history_exists_cb</function> callback is called to
+     check whether a timeline history file is present in the archives.
+
+<programlisting>
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file is
+     present in the archives.  If the file is not available in the archives,
+     the callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the timeline history file.
+    </para>
+
+    <para>
+     Some restore modules might not have a dedicated way to determine whether a
+     timeline history file exists.  For example,
+     <varname>restore_command</varname> only tells the server how to retrieve
+     files.  In this case, the <function>timeline_history_exists_cb</function>
+     callback should copy the file to <replaceable>path</replaceable>, which
+     contains the destination's relative path (including the file name), and
+     the restore module should set
+     <structfield>state->timeline_history_exists_cb_copies</structfield> to
+     <literal>true</literal>.  It is recommended to set this flag in the
+     module's <function>startup_cb</function> callback.  This flag tells the
+     server that it should verify that the file was successfully retrieved
+     after invoking this callback.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup-configured">
+    <title>Archive Cleanup Configured Callback</title>
+    <para>
+     The <function>archive_cleanup_configured_cb</function> callback is called
+     to determine whether the module is fully configured and ready to accept
+     requests to remove old WAL files from the archives (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>archive_cleanup_configured_cb</function> is defined, the server
+     always assumes the module is not configured to remove old WAL files.
+
+<programlisting>
+typedef bool (*ArchiveCleanupConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will remove old WAL
+     files by calling the <function>archive_cleanup_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>archive_cleanup_cb</function> callback to remove old WAL files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup">
+    <title>Archive Cleanup Callback</title>
+    <para>
+     The <function>archive_cleanup_cb</function> callback is called at every
+     restart point by the checkpointer process and is intended to provide a
+     mechanism for cleaning up old archived WAL files that are no longer
+     needed by the standby server.
+
+<programlisting>
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the
+     name of the file that includes the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end-configured">
+    <title>Recovery End Configured Callback</title>
+    <para>
+     The <function>recovery_end_configured_cb</function> callback is called to
+     determine whether the module is fully configured and ready to execute its
+     <function>recovery_end_cb</function> callback once at the end of recovery
+     (e.g., its configuration parameters are set to valid values).  If no
+     <function>recovery_end_configured_cb</function> is defined, the server
+     always assumes the module is not configured to execute its
+     <function>recovery_end_cb</function> callback.
+
+<programlisting>
+typedef bool (*RecoveryEndConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will execute the
+     <function>recovery_end_cb</function> callback once at the end of recovery.
+     If <literal>false</literal> is returned, the server will not use the
+     <function>recovery_end_cb</function> callback at the end of recovery.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end">
+    <title>Recovery End Callback</title>
+    <para>
+     The <function>recovery_end_cb</function> callback is called once at the
+     end of recovery and is intended to provide a mechanism for cleanup
+     following the end of replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-shutdown">
+    <title>Shutdown Callback</title>
+    <para>
+     The <function>shutdown_cb</function> callback is called when a process
+     that has loaded the restore module exits (e.g., after an error) or the
+     value of <xref linkend="guc-restore-library"/> changes.  If no
+     <function>shutdown_cb</function> is defined, no special action is taken in
+     these situations.  If the restore module has state, this callback should
+     free it to avoid leaks.
+
+<programlisting>
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+ </programlisting>
+    </para>
+   </sect3>
+  </sect2>
+ </sect1>
 </chapter>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index b3468eea3c..e1643f92e0 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1261,9 +1261,26 @@ SELECT * FROM pg_backup_stop(wait_for_archive => true);
    <para>
     The key part of all this is to set up a recovery configuration that
     describes how you want to recover and how far the recovery should
-    run.  The one thing that you absolutely must specify is the <varname>restore_command</varname>,
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>,
     which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    WAL file segments.
+   </para>
+
+   <para>
+    Like the <varname>archive_library</varname> parameter,
+    <varname>restore_library</varname> is a shared library.  Since such
+    libraries are written in <literal>C</literal>, creating your own may
+    require considerably more effort than writing a shell command.  However,
+    restore modules can be more performance than restoring via shell, and they
+    will have access to many useful server resources.  For more information
+    about creating a <varname>restore_library</varname>, see
+    <xref linkend="restore-modules"/>.
+   </para>
+
+   <para>
+    Like the <varname>archive_command</varname>,
+    <varname>restore_command</varname> is
     a shell command string.  It can contain <literal>%f</literal>, which is
     replaced by the name of the desired WAL file, and <literal>%p</literal>,
     which is replaced by the path name to copy the WAL file to.
@@ -1282,14 +1299,20 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
    </para>
 
    <para>
-    It is important that the command return nonzero exit status on failure.
-    The command <emphasis>will</emphasis> be called requesting files that are not
-    present in the archive; it must return nonzero when so asked.  This is not
-    an error condition.  An exception is that if the command was terminated by
+    It is important that the <varname>restore_command</varname> return nonzero
+    exit status on failure, or, if you are using a
+    <varname>restore_library</varname>, that the restore callbacks return
+    <literal>false</literal> on failure.  The command or library
+    <emphasis>will</emphasis> be called requesting files that are not
+    present in the archive; it must fail when so asked.  This is not
+    an error condition.  An exception is that if the
+    <varname>restore_command</varname> was terminated by
     a signal (other than <systemitem>SIGTERM</systemitem>, which is used as
     part of a database server shutdown) or an error by the shell (such as
     command not found), then recovery will abort and the server will not start
-    up.
+    up.  Likewise, if the restore callbacks provided by the
+    <varname>restore_library</varname> emit an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1313,7 +1336,8 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
     close as possible given the available WAL segments).  Therefore, a normal
     recovery will end with a <quote>file not found</quote> message, the exact text
     of the error message depending upon your choice of
-    <varname>restore_command</varname>.  You may also see an error message
+    <varname>restore_command</varname> or <varname>restore_library</varname>.
+    You may also see an error message
     at the start of recovery for a file named something like
     <filename>00000001.history</filename>.  This is also normal and does not
     indicate a problem in simple recovery situations; see
diff --git a/doc/src/sgml/basic-archive.sgml b/doc/src/sgml/basic-archive.sgml
index b4d43ced20..f3a5a502bd 100644
--- a/doc/src/sgml/basic-archive.sgml
+++ b/doc/src/sgml/basic-archive.sgml
@@ -8,17 +8,20 @@
  </indexterm>
 
  <para>
-  <filename>basic_archive</filename> is an example of an archive module.  This
-  module copies completed WAL segment files to the specified directory.  This
-  may not be especially useful, but it can serve as a starting point for
-  developing your own archive module.  For more information about archive
-  modules, see <xref linkend="archive-modules"/>.
+  <filename>basic_archive</filename> is an example of an archive and restore
+  module.  This module copies completed WAL segment files to or from the
+  specified directory.  This may not be especially useful, but it can serve as
+  a starting point for developing your own archive and restore modules.  For
+  more information about archive and restore modules, see\
+  <xref linkend="archive-and-restore-modules"/>.
  </para>
 
  <para>
-  In order to function, this module must be loaded via
+  For use as an archive module, this module must be loaded via
   <xref linkend="guc-archive-library"/>, and <xref linkend="guc-archive-mode"/>
-  must be enabled.
+  must be enabled.  For use as a restore module, this module must be loaded
+  via <xref linkend="guc-restore-library"/>, and recovery must be enabled (see
+  <xref linkend="runtime-config-wal-archive-recovery"/>).
  </para>
 
  <sect2 id="basic-archive-configuration-parameters">
@@ -34,11 +37,12 @@
     </term>
     <listitem>
      <para>
-      The directory where the server should copy WAL segment files.  This
-      directory must already exist.  The default is an empty string, which
-      effectively halts WAL archiving, but if <xref linkend="guc-archive-mode"/>
-      is enabled, the server will accumulate WAL segment files in the
-      expectation that a value will soon be provided.
+      The directory where the server should copy WAL segment files to or from.
+      This directory must already exist.  The default is an empty string,
+      which, when used for archiving, effectively halts WAL archival, but if
+      <xref linkend="guc-archive-mode"/> is enabled, the server will accumulate
+      WAL segment files in the expectation that a value will soon be provided.
+      When an empty string is used for recovery, restore will fail.
      </para>
     </listitem>
    </varlistentry>
@@ -46,7 +50,7 @@
 
   <para>
    These parameters must be set in <filename>postgresql.conf</filename>.
-   Typical usage might be:
+   Typical usage as an archive module might be:
   </para>
 
 <programlisting>
@@ -61,6 +65,7 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
+   When <filename>basic_archive</filename> is used as an archive module,
    Server crashes may leave temporary files with the prefix
    <filename>archtemp</filename> in the archive directory.  It is recommended to
    delete such files before restarting the server after a crash.  It is safe to
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 65a6e6c408..9a2570f87f 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3906,7 +3906,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
@@ -3934,7 +3935,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,
@@ -3969,7 +3971,42 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
 
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>restore_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-restore-library" xreflabel="restore_library">
+      <term><varname>restore_library</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>restore_library</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        The library to use for restore actions, including retrieving archived
+        files and executing tasks at restartpoints and at recovery end.  Either
+        <xref linkend="guc-restore-command"/> or
+        <varname>restore_library</varname> is required for archive recovery,
+        but optional for streaming replication.  If this parameter is set to an
+        empty string (the default), restoring via shell is enabled, and
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, and
+        <varname>recovery_end_command</varname> are used.  If both
+        <varname>restore_library</varname> and any of
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, or
+        <varname>recovery_end_command</varname> are set, an error will be
+        raised.  Otherwise, the specified shared library is used for restoring.
+        For more information, see <xref linkend="restore-modules"/>.
+       </para>
+
+       <para>
+        This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
        </para>
       </listitem>
      </varlistentry>
@@ -4014,7 +4051,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>
@@ -4043,7 +4083,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
        </para>
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>recovery_end_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index b48209fc2f..a86a9fe93a 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -627,7 +627,8 @@ protocol to make nodes agree on a serializable transactional order.
    <para>
     In standby mode, the server continuously applies WAL received from the
     primary server. The standby server can read WAL from a WAL archive
-    (see <xref linkend="guc-restore-command"/>) or directly from the primary
+    (see <xref linkend="guc-restore-command"/> and
+    <xref linkend="guc-restore-library"/>) or directly from the primary
     over a TCP connection (streaming replication). The standby server will
     also attempt to restore any WAL found in the standby cluster's
     <filename>pg_wal</filename> directory. That typically happens after a server
@@ -638,8 +639,10 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     At startup, the standby begins by restoring all WAL available in the
-    archive location, calling <varname>restore_command</varname>. Once it
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s callbacks.  Once it
     reaches the end of WAL available there and <varname>restore_command</varname>
+    or <varname>restore_library</varname>
     fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
     If that fails, and streaming replication has been configured, the
     standby tries to connect to the primary server and start streaming WAL
@@ -698,7 +701,8 @@ protocol to make nodes agree on a serializable transactional order.
     server (see <xref linkend="backup-pitr-recovery"/>). Create a file
     <link linkend="file-standby-signal"><filename>standby.signal</filename></link><indexterm><primary>standby.signal</primary></indexterm>
     in the standby's cluster data
-    directory. Set <xref linkend="guc-restore-command"/> to a simple command to copy files from
+    directory. Set <xref linkend="guc-restore-command"/> or
+    <xref linkend="guc-restore-library"/> to copy files from
     the WAL archive. If you plan to have multiple standby servers for high
     availability purposes, make sure that <varname>recovery_target_timeline</varname> is set to
     <literal>latest</literal> (the default), to make the standby server follow the timeline change
@@ -707,7 +711,8 @@ protocol to make nodes agree on a serializable transactional order.
 
    <note>
      <para>
-     <xref linkend="guc-restore-command"/> should return immediately
+     <xref linkend="guc-restore-command"/> and
+     <xref linkend="guc-restore-library"/> should return immediately
      if the file does not exist; the server will retry the command again if
      necessary.
     </para>
@@ -731,8 +736,9 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     If you're using a WAL archive, its size can be minimized using the <xref
-    linkend="guc-archive-cleanup-command"/> parameter to remove files that are no
-    longer required by the standby server.
+    linkend="guc-archive-cleanup-command"/> parameter or the
+    <xref linkend="guc-restore-library"/>'s archive cleanup callbacks to remove
+    files that are no longer required by the standby server.
     The <application>pg_archivecleanup</application> utility is designed specifically to
     be used with <varname>archive_cleanup_command</varname> in typical single-standby
     configurations, see <xref linkend="pgarchivecleanup"/>.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index e5d12f1d15..34a21c40a5 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -83,7 +83,7 @@
 #include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
-#include "restore/shell_restore.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
@@ -5178,18 +5178,19 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during
+	 * The callback is provided the archive file cutoff point for use during
 	 * log shipping replication.  All files earlier than this point can be
 	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_recovery_end_configured())
+	if (recovery_end_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_recovery_end(lastRestartPointFname);
+		RestoreCallbacks->recovery_end_cb(restore_module_state,
+										  lastRestartPointFname);
 	}
 
 	/*
@@ -7673,18 +7674,19 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Finally, execute archive-cleanup callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during
+	 * The callback is provided the archive file cutoff point for use during
 	 * log shipping replication.  All files earlier than this point can be
 	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_archive_cleanup_configured())
+	if (archive_cleanup_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_archive_cleanup(lastRestartPointFname);
+		RestoreCallbacks->archive_cleanup_cb(restore_module_state,
+											 lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index 5756f52124..3a05581495 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -23,14 +23,21 @@
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
 #include "common/archive.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/pgarch.h"
 #include "postmaster/startup.h"
 #include "replication/walsender.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
+#include "utils/memutils.h"
+
+char	   *restoreLibrary = "";
+const RestoreModuleCallbacks *RestoreCallbacks = NULL;
+RestoreModuleState *restore_module_state;
 
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
@@ -74,15 +81,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			if (!shell_restore_file_configured())
+			if (!restore_wal_segment_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			if (!shell_restore_file_configured())
+			if (!restore_timeline_history_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			if (!shell_restore_file_configured())
+			if (!timeline_history_exists_configured())
 				goto not_available;
 			break;
 	}
@@ -174,14 +181,17 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			ret = shell_restore_wal_segment(xlogfname, xlogpath,
-											lastRestartPointFname);
+			ret = RestoreCallbacks->restore_wal_segment_cb(restore_module_state,
+														   xlogfname, xlogpath,
+														   lastRestartPointFname);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->restore_timeline_history_cb(restore_module_state,
+																xlogfname, xlogpath);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->timeline_history_exists_cb(restore_module_state,
+															   xlogfname, xlogpath);
 			break;
 	}
 
@@ -189,6 +199,16 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 
 	if (ret)
 	{
+		/*
+		 * Some restore functions might not copy the file (e.g., checking
+		 * whether a timeline history file exists), but they can set a flag to
+		 * tell us if they do.  We only need to verify file existence if this
+		 * flag is enabled.
+		 */
+		if (archive_type == ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS &&
+			!restore_module_state->timeline_history_exists_cb_copies)
+			return true;
+
 		/*
 		 * command apparently succeeded, but let's make sure the file is
 		 * really there now and has the correct size.
@@ -630,3 +650,65 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the restore callbacks into our global RestoreCallbacks.  The
+ * caller is responsible for validating the combination of library/command
+ * parameters that are set (e.g., restore_command and restore_library cannot
+ * both be set).
+ */
+void
+LoadRestoreCallbacks(void)
+{
+	RestoreModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_restore_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RestoreModuleInit)
+			load_external_function(restoreLibrary, "_PG_restore_module_init",
+								   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("restore modules have to define the symbol "
+						"_PG_restore_module_init")));
+
+	RestoreCallbacks = (*init) ();
+
+	/* restore state should be freed before calling this function */
+	Assert(restore_module_state == NULL);
+	restore_module_state = (RestoreModuleState *)
+		MemoryContextAllocZero(TopMemoryContext,
+							   sizeof(RestoreModuleState));
+
+	if (RestoreCallbacks->startup_cb != NULL)
+		RestoreCallbacks->startup_cb(restore_module_state);
+}
+
+/*
+ * Call the shutdown callback of the loaded restore module, if defined.  Also,
+ * free the restore module state if it was allocated.
+ *
+ * Processes that load restore libraries should register this via
+ * before_shmem_exit().
+ */
+void
+call_restore_module_shutdown_cb(int code, Datum arg)
+{
+	if (RestoreCallbacks != NULL &&
+		RestoreCallbacks->shutdown_cb != NULL &&
+		restore_module_state != NULL)
+		RestoreCallbacks->shutdown_cb(restore_module_state);
+
+	if (restore_module_state != NULL)
+	{
+		pfree(restore_module_state);
+		restore_module_state = NULL;
+	}
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 07213b1897..1bf0d0ea11 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -51,6 +51,7 @@
 #include "replication/slot.h"
 #include "replication/slotsync.h"
 #include "replication/walreceiver.h"
+#include "restore/restore_module.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1117,18 +1118,21 @@ validateRecoveryParameters(void)
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			(!restore_wal_segment_configured() ||
+			 !restore_timeline_history_configured() ||
+			 !timeline_history_exists_configured()))
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
+					(errmsg("specified neither primary_conninfo nor restore_command nor a configured restore_library"),
 					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
 	}
 	else
 	{
-		if (recoveryRestoreCommand == NULL ||
-			strcmp(recoveryRestoreCommand, "") == 0)
+		if (!restore_wal_segment_configured() ||
+			!restore_timeline_history_configured() ||
+			!timeline_history_exists_configured())
 			ereport(FATAL,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("must specify restore_command when standby mode is not enabled")));
+					 errmsg("must specify restore_command or a configured restore_library when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 46197d56f8..47993aa35e 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -45,6 +45,7 @@
 #include "postmaster/bgwriter.h"
 #include "postmaster/interrupt.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -224,6 +225,25 @@ CheckpointerMain(void)
 												 ALLOCSET_DEFAULT_SIZES);
 	MemoryContextSwitchTo(checkpointer_context);
 
+	/*
+	 * Load restore_library so that we can use its archive-cleanup callback,
+	 * if one is defined.
+	 *
+	 * We also take this opportunity to set up the before_shmem_exit hook for
+	 * the shutdown callback and to check that only one of restore_library,
+	 * archive_cleanup_command is set.  Note that we emit a WARNING upon
+	 * detecting misconfigured parameters, and we clear the callbacks so that
+	 * the archive-cleanup functionality is skipped.  We judge this
+	 * functionality to not be important enough to require blocking
+	 * checkpoints or shutting down the server when the parameters are
+	 * misconfigured.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+										 archiveCleanupCommand, "archive_cleanup_command",
+										 WARNING))
+		LoadRestoreCallbacks();
+
 	/*
 	 * If an exception is encountered, processing resumes here.
 	 *
@@ -556,6 +576,10 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		bool		archive_cleanup_settings_changed = false;
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -571,6 +595,36 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		/*
+		 * If the archive-cleanup settings have changed, call the currently
+		 * loaded shutdown callback and clear all the restore callbacks.
+		 * There's presently no good way to unload a library besides
+		 * restarting the process, and there should be little harm in leaving
+		 * it around, so we just leave it loaded.
+		 */
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			archive_cleanup_settings_changed = true;
+			call_restore_module_shutdown_cb(0, (Datum) 0);
+			RestoreCallbacks = NULL;
+		}
+
+		/*
+		 * As in CheckpointerMain(), we only emit a WARNING if we detect that
+		 * both restore_library and archive_cleanup_command are set.  We do
+		 * this even if the archive-cleanup settings haven't changed to remind
+		 * the user about the misconfiguration.
+		 */
+		if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											 archiveCleanupCommand, "archive_cleanup_command",
+											 WARNING) &&
+			archive_cleanup_settings_changed)
+			LoadRestoreCallbacks();
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index e391bf6aa6..3cbd382e00 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -25,6 +25,7 @@
 #include "libpq/pqsignal.h"
 #include "miscadmin.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "storage/ipc.h"
 #include "storage/pmsignal.h"
 #include "storage/procsignal.h"
@@ -118,13 +119,17 @@ StartupProcShutdownHandler(SIGNAL_ARGS)
  * Re-read the config file.
  *
  * If one of the critical walreceiver options has changed, flag xlog.c
- * to restart it.
+ * to restart it.  Also, check for invalid combinations of the command/library
+ * parameters and reload the restore callbacks if necessary.
  */
 static void
 StartupRereadConfig(void)
 {
 	char	   *conninfo = pstrdup(PrimaryConnInfo);
 	char	   *slotname = pstrdup(PrimarySlotName);
+	char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+	char	   *prevRestoreCommand = pstrdup(recoveryRestoreCommand);
+	char	   *prevRecoveryEndCommand = pstrdup(recoveryEndCommand);
 	bool		tempSlot = wal_receiver_create_temp_slot;
 	bool		conninfoChanged;
 	bool		slotnameChanged;
@@ -146,6 +151,30 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	/*
+	 * If the restore settings have changed, call the currently loaded
+	 * shutdown callback and load the new callbacks.  There's presently no
+	 * good way to unload a library besides restarting the process, and there
+	 * should be little harm in leaving it around, so we just leave it loaded.
+	 */
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_restore_module_shutdown_cb(0, (Datum) 0);
+		LoadRestoreCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
@@ -255,6 +284,19 @@ StartupProcessMain(void)
 	 */
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	LoadRestoreCallbacks();
+
 	/*
 	 * Do what we came for.
 	 */
diff --git a/src/backend/restore/shell_restore.c b/src/backend/restore/shell_restore.c
index 42291625c1..be25f04044 100644
--- a/src/backend/restore/shell_restore.c
+++ b/src/backend/restore/shell_restore.c
@@ -3,7 +3,8 @@
  * shell_restore.c
  *
  * These restore functions use a user-specified shell command (e.g., the
- * restore_command GUC).
+ * restore_command GUC).  It is used as the default, but other modules may
+ * define their own restore logic.
  *
  * Copyright (c) 2024, PostgreSQL Global Development Group
  *
@@ -22,25 +23,76 @@
 #include "common/archive.h"
 #include "common/percentrepl.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static void shell_restore_startup(RestoreModuleState *state);
+static bool shell_restore_file_configured(RestoreModuleState *state);
 static bool shell_restore_file(const char *file, const char *path,
 							   const char *lastRestartPointFileName);
+static bool shell_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool shell_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool shell_archive_cleanup_configured(RestoreModuleState *state);
+static void shell_archive_cleanup(RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+static bool shell_recovery_end_configured(RestoreModuleState *state);
+static void shell_recovery_end(RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
 static void ExecuteRecoveryCommand(const char *command,
 								   const char *commandName,
 								   bool failOnSignal,
 								   uint32 wait_event_info,
 								   const char *lastRestartPointFileName);
 
+static const RestoreModuleCallbacks shell_restore_callbacks = {
+	.startup_cb = shell_restore_startup,
+	.restore_wal_segment_configured_cb = shell_restore_file_configured,
+	.restore_wal_segment_cb = shell_restore_wal_segment,
+	.restore_timeline_history_configured_cb = shell_restore_file_configured,
+	.restore_timeline_history_cb = shell_restore_timeline_history,
+	.timeline_history_exists_configured_cb = shell_restore_file_configured,
+	.timeline_history_exists_cb = shell_restore_timeline_history,
+	.archive_cleanup_configured_cb = shell_archive_cleanup_configured,
+	.archive_cleanup_cb = shell_archive_cleanup,
+	.recovery_end_configured_cb = shell_recovery_end_configured,
+	.recovery_end_cb = shell_recovery_end,
+	.shutdown_cb = NULL
+};
+
+/*
+ * shell_restore_init
+ *
+ * Returns the callbacks for restoring via shell.
+ */
+const RestoreModuleCallbacks *
+shell_restore_init(void)
+{
+	return &shell_restore_callbacks;
+}
+
+/*
+ * Indicates that our timeline_history_exists_cb only attempts to retrieve the
+ * file, so the caller should verify the file exists afterwards.
+ */
+static void
+shell_restore_startup(RestoreModuleState *state)
+{
+	state->timeline_history_exists_cb_copies = true;
+}
+
 /*
  * Attempt to execute a shell-based restore command to retrieve a WAL segment.
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_wal_segment(const char *file, const char *path,
+static bool
+shell_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
 						  const char *lastRestartPointFileName)
 {
 	return shell_restore_file(file, path, lastRestartPointFileName);
@@ -52,8 +104,9 @@ shell_restore_wal_segment(const char *file, const char *path,
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_timeline_history(const char *file, const char *path)
+static bool
+shell_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
 {
 	char		lastRestartPointFname[MAXPGPATH];
 
@@ -64,8 +117,8 @@ shell_restore_timeline_history(const char *file, const char *path)
 /*
  * Check whether restore_command is supplied.
  */
-bool
-shell_restore_file_configured(void)
+static bool
+shell_restore_file_configured(RestoreModuleState *state)
 {
 	return recoveryRestoreCommand[0] != '\0';
 }
@@ -152,8 +205,8 @@ shell_restore_file(const char *file, const char *path,
 /*
  * Check whether archive_cleanup_command is supplied.
  */
-bool
-shell_archive_cleanup_configured(void)
+static bool
+shell_archive_cleanup_configured(RestoreModuleState *state)
 {
 	return archiveCleanupCommand[0] != '\0';
 }
@@ -161,8 +214,9 @@ shell_archive_cleanup_configured(void)
 /*
  * Attempt to execute a shell-based archive cleanup command.
  */
-void
-shell_archive_cleanup(const char *lastRestartPointFileName)
+static void
+shell_archive_cleanup(RestoreModuleState *state,
+					  const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(archiveCleanupCommand, "archive_cleanup_command",
 						   false, WAIT_EVENT_ARCHIVE_CLEANUP_COMMAND,
@@ -172,8 +226,8 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 /*
  * Check whether recovery_end_command is supplied.
  */
-bool
-shell_recovery_end_configured(void)
+static bool
+shell_recovery_end_configured(RestoreModuleState *state)
 {
 	return recoveryEndCommand[0] != '\0';
 }
@@ -181,8 +235,9 @@ shell_recovery_end_configured(void)
 /*
  * Attempt to execute a shell-based end-of-recovery command.
  */
-void
-shell_recovery_end(const char *lastRestartPointFileName)
+static void
+shell_recovery_end(RestoreModuleState *state,
+				   const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(recoveryEndCommand, "recovery_end_command", true,
 						   WAIT_EVENT_RECOVERY_END_COMMAND,
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 57d9de4dd9..295cde9247 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -70,6 +70,7 @@
 #include "replication/slot.h"
 #include "replication/slotsync.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -3946,6 +3947,16 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"restore_library", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
+			gettext_noop("Sets the library that will be called for restore actions."),
+			NULL
+		},
+		&restoreLibrary,
+		"",
+		NULL, NULL, NULL
+	},
+
 	{
 		{"archive_cleanup_command", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
 			gettext_noop("Sets the shell command that will be executed at every restart point."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 2244ee52f7..5265857519 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -284,6 +284,7 @@
 				# placeholders: %p = path of file to restore
 				#               %f = file name only
 				# e.g. 'cp /mnt/server/archivedir/%f %p'
+#restore_library = ''		# library to use for restore actions
 #archive_cleanup_command = ''	# command to execute at every restartpoint
 #recovery_end_command = ''	# command to execute at completion of recovery
 
diff --git a/src/include/restore/restore_module.h b/src/include/restore/restore_module.h
new file mode 100644
index 0000000000..cc148e855a
--- /dev/null
+++ b/src/include/restore/restore_module.h
@@ -0,0 +1,143 @@
+/*-------------------------------------------------------------------------
+ *
+ * restore_module.h
+ *		Exports for restore modules.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * src/include/restore/restore_module.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef _RESTORE_MODULE_H
+#define _RESTORE_MODULE_H
+
+/*
+ * The value of the restore_library GUC.
+ */
+extern PGDLLIMPORT char *restoreLibrary;
+
+typedef struct RestoreModuleState
+{
+	/*
+	 * Private data pointer for use by a restore module.  This can be used to
+	 * store state for the module that will be passed to each of its
+	 * callbacks.
+	 */
+	void	   *private_data;
+
+	/*
+	 * Indicates whether the callback that checks for timeline existence
+	 * merely copies the file.  If set to true, the server will verify that
+	 * the timeline history file exists after the callback returns.
+	 */
+	bool		timeline_history_exists_cb_copies;
+} RestoreModuleState;
+
+/*
+ * Restore module callbacks
+ *
+ * These callback functions should be defined by restore libraries and returned
+ * via _PG_restore_module_init().  For more information about the purpose of
+ * each callback, refer to the restore modules documentation.
+ */
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state,
+									 const char *file, const char *path,
+									 const char *lastRestartPointFileName);
+typedef bool (*RestoreTimelineHistoryConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state,
+										  const char *file, const char *path);
+typedef bool (*TimelineHistoryExistsConfiguredCB) (RestoreModuleState *state);
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state,
+										 const char *file, const char *path);
+typedef bool (*ArchiveCleanupConfiguredCB) (RestoreModuleState *state);
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+typedef bool (*RecoveryEndConfiguredCB) (RestoreModuleState *state);
+typedef void (*RecoveryEndCB) (RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+
+typedef struct RestoreModuleCallbacks
+{
+	RestoreStartupCB startup_cb;
+	RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+	RestoreWalSegmentCB restore_wal_segment_cb;
+	RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+	RestoreTimelineHistoryCB restore_timeline_history_cb;
+	TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+	TimelineHistoryExistsCB timeline_history_exists_cb;
+	ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+	ArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndConfiguredCB recovery_end_configured_cb;
+	RecoveryEndCB recovery_end_cb;
+	RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+
+/*
+ * Type of the shared library symbol _PG_restore_module_init that is looked up
+ * when loading a restore library.
+ */
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+
+extern PGDLLEXPORT const RestoreModuleCallbacks *_PG_restore_module_init(void);
+
+extern void LoadRestoreCallbacks(void);
+extern void call_restore_module_shutdown_cb(int code, Datum arg);
+
+extern const RestoreModuleCallbacks *RestoreCallbacks;
+extern RestoreModuleState *restore_module_state;
+
+/*
+ * The following *_configured() functions are convenient wrappers around the
+ * *_configured_cb() callback functions with additional defensive NULL checks.
+ */
+
+static inline bool
+restore_wal_segment_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->restore_wal_segment_configured_cb != NULL &&
+		RestoreCallbacks->restore_wal_segment_cb != NULL &&
+		RestoreCallbacks->restore_wal_segment_configured_cb(restore_module_state);
+}
+
+static inline bool
+restore_timeline_history_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->restore_timeline_history_configured_cb != NULL &&
+		RestoreCallbacks->restore_timeline_history_cb != NULL &&
+		RestoreCallbacks->restore_timeline_history_configured_cb(restore_module_state);
+}
+
+static inline bool
+timeline_history_exists_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->timeline_history_exists_configured_cb != NULL &&
+		RestoreCallbacks->timeline_history_exists_cb != NULL &&
+		RestoreCallbacks->timeline_history_exists_configured_cb(restore_module_state);
+}
+
+static inline bool
+archive_cleanup_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->archive_cleanup_configured_cb != NULL &&
+		RestoreCallbacks->archive_cleanup_cb != NULL &&
+		RestoreCallbacks->archive_cleanup_configured_cb(restore_module_state);
+}
+
+static inline bool
+recovery_end_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->recovery_end_configured_cb != NULL &&
+		RestoreCallbacks->recovery_end_cb != NULL &&
+		RestoreCallbacks->recovery_end_configured_cb(restore_module_state);
+}
+
+#endif							/* _RESTORE_MODULE_H */
diff --git a/src/include/restore/shell_restore.h b/src/include/restore/shell_restore.h
index 28b5b7bc92..6352623866 100644
--- a/src/include/restore/shell_restore.h
+++ b/src/include/restore/shell_restore.h
@@ -12,15 +12,13 @@
 #ifndef _SHELL_RESTORE_H
 #define _SHELL_RESTORE_H
 
-extern bool shell_restore_file_configured(void);
-extern bool shell_restore_wal_segment(const char *file, const char *path,
-									  const char *lastRestartPointFileName);
-extern bool shell_restore_timeline_history(const char *file, const char *path);
+#include "restore/restore_module.h"
 
-extern bool shell_archive_cleanup_configured(void);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-
-extern bool shell_recovery_end_configured(void);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Since the logic for restoring via shell commands is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern const RestoreModuleCallbacks *shell_restore_init(void);
 
 #endif							/* _SHELL_RESTORE_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 2f975ac53a..8e190a0c79 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2388,6 +2388,8 @@ ResourceOwnerDesc
 ResourceReleaseCallback
 ResourceReleaseCallbackItem
 ResourceReleasePhase
+RestoreModuleCallbacks
+RestoreModuleState
 RestoreOptions
 RestorePass
 RestrictInfo
-- 
2.25.1


--oyUTqETQ0mS9luUI--





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

* [PATCH v20 5/5] introduce restore_library
@ 2023-02-16 06:06  Nathan Bossart <[email protected]>
  0 siblings, 0 replies; 28+ messages in thread

From: Nathan Bossart @ 2023-02-16 06:06 UTC (permalink / raw)

---
 contrib/basic_archive/Makefile                |   2 +
 contrib/basic_archive/basic_archive.c         | 153 +++++++-
 contrib/basic_archive/meson.build             |   5 +
 contrib/basic_archive/t/001_restore.pl        |  44 +++
 doc/src/sgml/archive-and-restore-modules.sgml | 356 +++++++++++++++++-
 doc/src/sgml/backup.sgml                      |  40 +-
 doc/src/sgml/basic-archive.sgml               |  31 +-
 doc/src/sgml/config.sgml                      |  53 ++-
 doc/src/sgml/high-availability.sgml           |  18 +-
 src/backend/access/transam/xlog.c             |  20 +-
 src/backend/access/transam/xlogarchive.c      |  96 ++++-
 src/backend/access/transam/xlogrecovery.c     |  14 +-
 src/backend/postmaster/checkpointer.c         |  54 +++
 src/backend/postmaster/startup.c              |  44 ++-
 src/backend/restore/shell_restore.c           |  85 ++++-
 src/backend/utils/misc/guc_tables.c           |  11 +
 src/backend/utils/misc/postgresql.conf.sample |   1 +
 src/include/restore/restore_module.h          | 143 +++++++
 src/include/restore/shell_restore.h           |  16 +-
 src/tools/pgindent/typedefs.list              |   2 +
 20 files changed, 1104 insertions(+), 84 deletions(-)
 create mode 100644 contrib/basic_archive/t/001_restore.pl
 create mode 100644 src/include/restore/restore_module.h

diff --git a/contrib/basic_archive/Makefile b/contrib/basic_archive/Makefile
index 100ed81f12..e61f7d676f 100644
--- a/contrib/basic_archive/Makefile
+++ b/contrib/basic_archive/Makefile
@@ -10,6 +10,8 @@ REGRESS_OPTS = --temp-config $(top_srcdir)/contrib/basic_archive/basic_archive.c
 # typical installcheck users do not have (e.g. buildfarm clients).
 NO_INSTALLCHECK = 1
 
+TAP_TESTS = 1
+
 ifdef USE_PGXS
 PG_CONFIG = pg_config
 PGXS := $(shell $(PG_CONFIG) --pgxs)
diff --git a/contrib/basic_archive/basic_archive.c b/contrib/basic_archive/basic_archive.c
index 6b102e9072..e0d8dce364 100644
--- a/contrib/basic_archive/basic_archive.c
+++ b/contrib/basic_archive/basic_archive.c
@@ -17,6 +17,11 @@
  * a file is successfully archived and then the system crashes before
  * a durable record of the success has been made.
  *
+ * This file also demonstrates a basic restore library implementation that
+ * is roughly equivalent to the following shell command:
+ *
+ *		cp /path/to/archivedir/%f %p
+ *
  * Copyright (c) 2022-2024, PostgreSQL Global Development Group
  *
  * IDENTIFICATION
@@ -33,6 +38,7 @@
 #include "archive/archive_module.h"
 #include "common/int.h"
 #include "miscadmin.h"
+#include "restore/restore_module.h"
 #include "storage/copydir.h"
 #include "storage/fd.h"
 #include "utils/guc.h"
@@ -54,6 +60,16 @@ static void basic_archive_file_internal(const char *file, const char *path);
 static bool check_archive_directory(char **newval, void **extra, GucSource source);
 static bool compare_files(const char *file1, const char *file2);
 static void basic_archive_shutdown(ArchiveModuleState *state);
+static bool basic_restore_configured(RestoreModuleState *state);
+static bool basic_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool basic_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool basic_restore_file(const char *file, const char *path);
+static bool basic_restore_timeline_history_exists(RestoreModuleState *state,
+												  const char *file,
+												  const char *path);
 
 static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.startup_cb = basic_archive_startup,
@@ -62,6 +78,21 @@ static const ArchiveModuleCallbacks basic_archive_callbacks = {
 	.shutdown_cb = basic_archive_shutdown
 };
 
+static const RestoreModuleCallbacks basic_restore_callbacks = {
+	.startup_cb = NULL,
+	.restore_wal_segment_configured_cb = basic_restore_configured,
+	.restore_wal_segment_cb = basic_restore_wal_segment,
+	.restore_timeline_history_configured_cb = basic_restore_configured,
+	.restore_timeline_history_cb = basic_restore_timeline_history,
+	.timeline_history_exists_configured_cb = basic_restore_configured,
+	.timeline_history_exists_cb = basic_restore_timeline_history_exists,
+	.archive_cleanup_configured_cb = NULL,
+	.archive_cleanup_cb = NULL,
+	.recovery_end_configured_cb = NULL,
+	.recovery_end_cb = NULL,
+	.shutdown_cb = NULL
+};
+
 /*
  * _PG_init
  *
@@ -71,7 +102,7 @@ void
 _PG_init(void)
 {
 	DefineCustomStringVariable("basic_archive.archive_directory",
-							   gettext_noop("Archive file destination directory."),
+							   gettext_noop("Archive file source/destination directory."),
 							   NULL,
 							   &archive_directory,
 							   "",
@@ -93,6 +124,17 @@ _PG_archive_module_init(void)
 	return &basic_archive_callbacks;
 }
 
+/*
+ * _PG_restore_module_init
+ *
+ * Returns the module's restore callbacks.
+ */
+const RestoreModuleCallbacks *
+_PG_restore_module_init(void)
+{
+	return &basic_restore_callbacks;
+}
+
 /*
  * basic_archive_startup
  *
@@ -431,3 +473,112 @@ basic_archive_shutdown(ArchiveModuleState *state)
 	pfree(data);
 	state->private_data = NULL;
 }
+
+/*
+ * basic_restore_configured
+ *
+ * Checks that archive_directory is not blank.
+ */
+static bool
+basic_restore_configured(RestoreModuleState *state)
+{
+	return archive_directory != NULL && archive_directory[0] != '\0';
+}
+
+/*
+ * basic_restore_wal_segment
+ *
+ * Retrieves one archived WAL segment.
+ */
+static bool
+basic_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
+						  const char *lastRestartPointFileName)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_timeline_history
+ *
+ * Retrieves one timeline history file.
+ */
+static bool
+basic_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
+{
+	return basic_restore_file(file, path);
+}
+
+/*
+ * basic_restore_file
+ *
+ * Retrieves one file.
+ */
+static bool
+basic_restore_file(const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("restoring \"%s\" via basic_archive",
+					file)));
+
+	/*
+	 * If the file doesn't exist, return false to indicate that there are no
+	 * more files to restore.
+	 */
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	copy_file(archive_path, path);
+
+	ereport(DEBUG1,
+			(errmsg("restored \"%s\" via basic_archive",
+					file)));
+	return true;
+}
+
+/*
+ * basic_restore_timeline_history_exists
+ *
+ * Check whether a timeline history file is present in the archive directory.
+ */
+static bool
+basic_restore_timeline_history_exists(RestoreModuleState *state,
+									  const char *file, const char *path)
+{
+	char		archive_path[MAXPGPATH];
+	struct stat st;
+
+	ereport(DEBUG1,
+			(errmsg("checking existence of \"%s\" via basic_archive",
+					file)));
+
+	snprintf(archive_path, MAXPGPATH, "%s/%s", archive_directory, file);
+	if (stat(archive_path, &st) != 0)
+	{
+		int			elevel = (errno == ENOENT) ? DEBUG1 : ERROR;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not stat file \"%s\": %m",
+						archive_path)));
+		return false;
+	}
+
+	ereport(DEBUG1,
+			(errmsg("verified existence of \"%s\" via basic_archive",
+					file)));
+	return true;
+}
diff --git a/contrib/basic_archive/meson.build b/contrib/basic_archive/meson.build
index cc2f62bf36..8e36fa7ed6 100644
--- a/contrib/basic_archive/meson.build
+++ b/contrib/basic_archive/meson.build
@@ -31,4 +31,9 @@ tests += {
     # typical installcheck users do not have (e.g. buildfarm clients).
     'runningcheck': false,
   },
+  'tap': {
+    'tests': [
+      't/001_restore.pl',
+    ],
+  },
 }
diff --git a/contrib/basic_archive/t/001_restore.pl b/contrib/basic_archive/t/001_restore.pl
new file mode 100644
index 0000000000..6c01f736cf
--- /dev/null
+++ b/contrib/basic_archive/t/001_restore.pl
@@ -0,0 +1,44 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# start a node
+my $node = PostgreSQL::Test::Cluster->new('node');
+$node->init(has_archiving => 1, allows_streaming => 1);
+my $archive_dir = $node->archive_dir;
+$archive_dir =~ s!\\!/!g if $PostgreSQL::Test::Utils::windows_os;
+$node->append_conf('postgresql.conf', "archive_command = ''");
+$node->append_conf('postgresql.conf', "archive_library = 'basic_archive'");
+$node->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$node->start;
+
+# backup the node
+my $backup = 'backup';
+$node->backup($backup);
+
+# generate some new WAL files
+$node->safe_psql('postgres', "CREATE TABLE test (a INT);");
+$node->safe_psql('postgres', "SELECT pg_switch_wal();");
+$node->safe_psql('postgres', "INSERT INTO test VALUES (1);");
+
+# shut down the node (this should archive all WAL files)
+$node->stop;
+
+# restore from the backup
+my $restore = PostgreSQL::Test::Cluster->new('restore');
+$restore->init_from_backup($node, $backup, has_restoring => 1, standby => 0);
+$restore->append_conf('postgresql.conf', "restore_command = ''");
+$restore->append_conf('postgresql.conf', "restore_library = 'basic_archive'");
+$restore->append_conf('postgresql.conf', "basic_archive.archive_directory = '$archive_dir'");
+$restore->start;
+
+# ensure post-backup WAL is replayed
+my $result = $restore->poll_query_until("postgres", "SELECT count(*) = 1 FROM test;");
+is($result, "1", "check restore content");
+
+done_testing();
diff --git a/doc/src/sgml/archive-and-restore-modules.sgml b/doc/src/sgml/archive-and-restore-modules.sgml
index a52d15082d..15be548a73 100644
--- a/doc/src/sgml/archive-and-restore-modules.sgml
+++ b/doc/src/sgml/archive-and-restore-modules.sgml
@@ -8,15 +8,17 @@
 
  <para>
   PostgreSQL provides infrastructure to create custom modules for continuous
-  archiving (see <xref linkend="continuous-archiving"/>).  While archiving via
-  a shell command (i.e., <xref linkend="guc-archive-command"/>) is much
-  simpler, a custom archive module will often be considerably more robust and
-  performant.
+  archiving and recovery (see <xref linkend="continuous-archiving"/>).  While a
+  shell command (i.e., <xref linkend="guc-archive-command"/>,
+  <xref linkend="guc-restore-command"/>) is much simpler, a custom module will
+  often be considerably more robust and performant.
  </para>
 
  <para>
   The <filename>contrib/basic_archive</filename> module contains a working
-  example, which demonstrates some useful techniques.
+  example, which demonstrates some useful techniques.  As demonstrated in
+  <filename>basic_archive</filename> a single module may serve as both an
+  archive module and a restore module.
  </para>
 
  <sect1 id="archive-modules">
@@ -178,4 +180,348 @@ typedef void (*ArchiveShutdownCB) (ArchiveModuleState *state);
    </sect3>
   </sect2>
  </sect1>
+
+ <sect1 id="restore-modules">
+  <title>Restore Modules</title>
+  <indexterm zone="restore-modules">
+   <primary>Restore Modules</primary>
+  </indexterm>
+
+  <para>
+   When a custom <xref linkend="guc-restore-library"/> is configured,
+   PostgreSQL will use the module for restore actions.  It is
+   ultimately up to the module to decide how to accomplish each task,
+   but some recommendations are listed at
+   <xref linkend="backup-pitr-recovery"/>.
+  </para>
+
+  <para>
+   Restore modules must at least consist of an initialization function
+   (see <xref linkend="restore-module-init"/>) and the required callbacks (see
+   <xref linkend="restore-module-callbacks"/>).  However, restore modules are
+   also permitted to do much more (e.g., declare GUCs and register background
+   workers).
+  </para>
+
+  <sect2 id="restore-module-init">
+   <title>Initialization Functions</title>
+   <indexterm zone="restore-module-init">
+    <primary>_PG_restore_module_init</primary>
+   </indexterm>
+
+   <para>
+    An restore library is loaded by dynamically loading a shared library with
+    the <xref linkend="guc-restore-library"/>'s name as the library base name.
+    The normal library search path is used to locate the library.  To provide
+    the required restore module callbacks and to indicate that the library is
+    actually a restore module, it needs to provide a function named
+    <function>_PG_restore_module_init</function>.  The result of the function
+    must be a pointer to a struct of type
+    <structname>RestoreModuleCallbacks</structname>, which contains everything
+    that the core code needs to know how to make use of the restore module.
+    The return value needs to be of server lifetime, which is typically
+    achieved by defining it as a <literal>static const</literal> variable in
+    global scope.
+
+<programlisting>
+typedef struct RestoreModuleCallbacks
+{
+    RestoreStartupCB startup_cb;
+    RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+    RestoreWalSegmentCB restore_wal_segment_cb;
+    RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+    RestoreTimelineHistoryCB restore_timeline_history_cb;
+    TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+    TimelineHistoryExistsCB timeline_history_exists_cb;
+    ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+    ArchiveCleanupCB archive_cleanup_cb;
+    RecoveryEndConfiguredCB recovery_end_configured_cb;
+    RecoveryEndCB recovery_end_cb;
+    RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+</programlisting>
+
+    The <function>restore_wal_segment_configured_cb</function>,
+    <function>restore_wal_segment_cb</function>,
+    <function>restore_timeline_history_configured_cb</function>,
+    <function>restore_timeline_history_cb</function>,
+    <function>timeline_history_exists_configured_cb</function>, and
+    <function>timeline_history_exists_cb</function> callbacks are required for
+    archive recovery but optional for streaming replication.  The others are
+    always optional.
+   </para>
+  </sect2>
+
+  <sect2 id="restore-module-callbacks">
+   <title>Restore Module Callbacks</title>
+
+   <para>
+    The restore callbacks define the actual behavior of the module.  The server
+    will call them as required to execute restore actions.
+   </para>
+
+   <sect3 id="restore-module-startup">
+    <title>Startup Callback</title>
+
+    <para>
+     The <function>startup_cb</function> callback is called shortly after the
+     module is loaded.  This callback can be used to perform any additional
+     initialization required.  If the restore module has state, it can use
+     <structfield>state->private_data</structfield> to store it.
+
+<programlisting>
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+</programlisting>
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment-configured">
+    <title>Restore WAL Segment Configured Callback</title>
+    <para>
+     The <function>restore_wal_segment_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to retrieve WAL files (e.g., its configuration parameters
+     are set to valid values).  If no
+     <function>restore_wal_segment_configured_cb</function> is defined, the
+     server always assumes the module is not configured to retrieve WAL files.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve WAL files
+     by calling the <function>restore_wal_segment_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_wal_segment_cb</function> callback to retrieve WAL
+     files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-wal-segment">
+    <title>Restore WAL Segment Callback</title>
+    <para>
+     The <function>restore_wal_segment_cb</function> callback is called to
+     retrieve a single archived segment of the WAL file series for archive
+     recovery or streaming replication.
+
+<programlisting>
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state, const char *file, const char *path, const char *lastRestartPointFileName);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point.  That is the earliest
+     file that must be kept to allow a restore to be restartable, so this
+     information can be used to truncate the archive to just the minimum
+     required to support restarting from the current restore.
+     <replaceable>lastRestartPointFileName</replaceable> is typically only used
+     by warm-standby configurations (see <xref linkend="warm-standby"/>).  Note
+     that if multiple standby servers are restoring from the same archive
+     directory, you will need to ensure that you do not delete WAL files until
+     they are no longer needed by any of the servers.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history-configured">
+    <title>Restore Timeline History Configured Callback</title>
+    <para>
+     The <function>restore_timeline_history_configured_cb</function> callback
+     is called to determine whether the module is fully configured and ready to
+     accept requests to retrieve timeline history files (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>restore_timeline_history_configured_cb</function> is defined,
+     the server always assumes the module is not configured to retrieve
+     timeline history files.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will retrieve timeline
+     history files by calling the
+     <function>restore_timeline_history_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>restore_timeline_history_cb</function> callback to retrieve
+     timeline history files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-restore-timeline-history">
+    <title>Restore Timeline History Callback</title>
+    <para>
+     The <function>restore_timeline_history_cb</function> callback is called to
+     retrieve a single archived timeline history file for archive recovery or
+     streaming replication.
+
+<programlisting>
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file was
+     successfully retrieved.  If the file is not available in the archives, the
+     callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the WAL file to retrieve, while <replaceable>path</replaceable>
+     contains the destination's relative path (including the file name).
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists-configured">
+    <title>Timeline History Exists Configured Callback</title>
+    <para>
+     The <function>timeline_history_exists_configured_cb</function> callback is
+     called to determine whether the module is fully configured and ready to
+     accept requests to determine whether a timeline history file exists in the
+     archives (e.g., its configuration parameters are set to valid values).  If
+     no <function>timeline_history_exists_configured_cb</function> is defined,
+     the server always assumes the module is not configured to check whether
+     certain timeline history files exist.
+
+<programlisting>
+typedef bool (*TimelineHistoryConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will check whether
+     certain timeline history files are present in the archives by calling the
+     <function>timeline_history_exists_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>timeline_history_exists_cb</function> callback to check if
+     timeline history files exist in the archives.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-timeline-history-exists">
+    <title>Timeline History Exists Callback</title>
+    <para>
+     The <function>timeline_history_exists_cb</function> callback is called to
+     check whether a timeline history file is present in the archives.
+
+<programlisting>
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state, const char *file, const char *path);
+</programlisting>
+
+     This callback must return <literal>true</literal> only if the file is
+     present in the archives.  If the file is not available in the archives,
+     the callback must return <literal>false</literal>.
+     <replaceable>file</replaceable> will contain just the file name
+     of the timeline history file.
+    </para>
+
+    <para>
+     Some restore modules might not have a dedicated way to determine whether a
+     timeline history file exists.  For example,
+     <varname>restore_command</varname> only tells the server how to retrieve
+     files.  In this case, the <function>timeline_history_exists_cb</function>
+     callback should copy the file to <replaceable>path</replaceable>, which
+     contains the destination's relative path (including the file name), and
+     the restore module should set
+     <structfield>state->timeline_history_exists_cb_copies</structfield> to
+     <literal>true</literal>.  It is recommended to set this flag in the
+     module's <function>startup_cb</function> callback.  This flag tells the
+     server that it should verify that the file was successfully retrieved
+     after invoking this callback.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup-configured">
+    <title>Archive Cleanup Configured Callback</title>
+    <para>
+     The <function>archive_cleanup_configured_cb</function> callback is called
+     to determine whether the module is fully configured and ready to accept
+     requests to remove old WAL files from the archives (e.g., its
+     configuration parameters are set to valid values).  If no
+     <function>archive_cleanup_configured_cb</function> is defined, the server
+     always assumes the module is not configured to remove old WAL files.
+
+<programlisting>
+typedef bool (*ArchiveCleanupConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will remove old WAL
+     files by calling the <function>archive_cleanup_cb</function> callback.  If
+     <literal>false</literal> is returned, the server will not use the
+     <function>archive_cleanup_cb</function> callback to remove old WAL files.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-archive-cleanup">
+    <title>Archive Cleanup Callback</title>
+    <para>
+     The <function>archive_cleanup_cb</function> callback is called at every
+     restart point by the checkpointer process and is intended to provide a
+     mechanism for cleaning up old archived WAL files that are no longer
+     needed by the standby server.
+
+<programlisting>
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the
+     name of the file that includes the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end-configured">
+    <title>Recovery End Configured Callback</title>
+    <para>
+     The <function>recovery_end_configured_cb</function> callback is called to
+     determine whether the module is fully configured and ready to execute its
+     <function>recovery_end_cb</function> callback once at the end of recovery
+     (e.g., its configuration parameters are set to valid values).  If no
+     <function>recovery_end_configured_cb</function> is defined, the server
+     always assumes the module is not configured to execute its
+     <function>recovery_end_cb</function> callback.
+
+<programlisting>
+typedef bool (*RecoveryEndConfiguredCB)  (RestoreModuleState *state);
+</programlisting>
+
+     If <literal>true</literal> is returned, the server will execute the
+     <function>recovery_end_cb</function> callback once at the end of recovery.
+     If <literal>false</literal> is returned, the server will not use the
+     <function>recovery_end_cb</function> callback at the end of recovery.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-recovery-end">
+    <title>Recovery End Callback</title>
+    <para>
+     The <function>recovery_end_cb</function> callback is called once at the
+     end of recovery and is intended to provide a mechanism for cleanup
+     following the end of replication or recovery.
+
+<programlisting>
+typedef void (*RecoveryEndCB) (RestoreModuleState *state, const char *lastRestartPointFileName);
+</programlisting>
+
+     <replaceable>lastRestartPointFileName</replaceable> will contain the name
+     of the file containing the last valid restart point, like in
+     <link linkend="restore-module-restore-wal-segment"><function>restore_wal_segment_cb</function></link>.
+    </para>
+   </sect3>
+
+   <sect3 id="restore-module-shutdown">
+    <title>Shutdown Callback</title>
+    <para>
+     The <function>shutdown_cb</function> callback is called when a process
+     that has loaded the restore module exits (e.g., after an error) or the
+     value of <xref linkend="guc-restore-library"/> changes.  If no
+     <function>shutdown_cb</function> is defined, no special action is taken in
+     these situations.  If the restore module has state, this callback should
+     free it to avoid leaks.
+
+<programlisting>
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+ </programlisting>
+    </para>
+   </sect3>
+  </sect2>
+ </sect1>
 </chapter>
diff --git a/doc/src/sgml/backup.sgml b/doc/src/sgml/backup.sgml
index b3468eea3c..e1643f92e0 100644
--- a/doc/src/sgml/backup.sgml
+++ b/doc/src/sgml/backup.sgml
@@ -1261,9 +1261,26 @@ SELECT * FROM pg_backup_stop(wait_for_archive => true);
    <para>
     The key part of all this is to set up a recovery configuration that
     describes how you want to recover and how far the recovery should
-    run.  The one thing that you absolutely must specify is the <varname>restore_command</varname>,
+    run.  The one thing that you absolutely must specify is either
+    <varname>restore_command</varname> or a <varname>restore_library</varname>,
     which tells <productname>PostgreSQL</productname> how to retrieve archived
-    WAL file segments.  Like the <varname>archive_command</varname>, this is
+    WAL file segments.
+   </para>
+
+   <para>
+    Like the <varname>archive_library</varname> parameter,
+    <varname>restore_library</varname> is a shared library.  Since such
+    libraries are written in <literal>C</literal>, creating your own may
+    require considerably more effort than writing a shell command.  However,
+    restore modules can be more performance than restoring via shell, and they
+    will have access to many useful server resources.  For more information
+    about creating a <varname>restore_library</varname>, see
+    <xref linkend="restore-modules"/>.
+   </para>
+
+   <para>
+    Like the <varname>archive_command</varname>,
+    <varname>restore_command</varname> is
     a shell command string.  It can contain <literal>%f</literal>, which is
     replaced by the name of the desired WAL file, and <literal>%p</literal>,
     which is replaced by the path name to copy the WAL file to.
@@ -1282,14 +1299,20 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
    </para>
 
    <para>
-    It is important that the command return nonzero exit status on failure.
-    The command <emphasis>will</emphasis> be called requesting files that are not
-    present in the archive; it must return nonzero when so asked.  This is not
-    an error condition.  An exception is that if the command was terminated by
+    It is important that the <varname>restore_command</varname> return nonzero
+    exit status on failure, or, if you are using a
+    <varname>restore_library</varname>, that the restore callbacks return
+    <literal>false</literal> on failure.  The command or library
+    <emphasis>will</emphasis> be called requesting files that are not
+    present in the archive; it must fail when so asked.  This is not
+    an error condition.  An exception is that if the
+    <varname>restore_command</varname> was terminated by
     a signal (other than <systemitem>SIGTERM</systemitem>, which is used as
     part of a database server shutdown) or an error by the shell (such as
     command not found), then recovery will abort and the server will not start
-    up.
+    up.  Likewise, if the restore callbacks provided by the
+    <varname>restore_library</varname> emit an <literal>ERROR</literal> or
+    <literal>FATAL</literal>, recovery will abort and the server won't start.
    </para>
 
    <para>
@@ -1313,7 +1336,8 @@ restore_command = 'cp /mnt/server/archivedir/%f %p'
     close as possible given the available WAL segments).  Therefore, a normal
     recovery will end with a <quote>file not found</quote> message, the exact text
     of the error message depending upon your choice of
-    <varname>restore_command</varname>.  You may also see an error message
+    <varname>restore_command</varname> or <varname>restore_library</varname>.
+    You may also see an error message
     at the start of recovery for a file named something like
     <filename>00000001.history</filename>.  This is also normal and does not
     indicate a problem in simple recovery situations; see
diff --git a/doc/src/sgml/basic-archive.sgml b/doc/src/sgml/basic-archive.sgml
index b4d43ced20..f3a5a502bd 100644
--- a/doc/src/sgml/basic-archive.sgml
+++ b/doc/src/sgml/basic-archive.sgml
@@ -8,17 +8,20 @@
  </indexterm>
 
  <para>
-  <filename>basic_archive</filename> is an example of an archive module.  This
-  module copies completed WAL segment files to the specified directory.  This
-  may not be especially useful, but it can serve as a starting point for
-  developing your own archive module.  For more information about archive
-  modules, see <xref linkend="archive-modules"/>.
+  <filename>basic_archive</filename> is an example of an archive and restore
+  module.  This module copies completed WAL segment files to or from the
+  specified directory.  This may not be especially useful, but it can serve as
+  a starting point for developing your own archive and restore modules.  For
+  more information about archive and restore modules, see\
+  <xref linkend="archive-and-restore-modules"/>.
  </para>
 
  <para>
-  In order to function, this module must be loaded via
+  For use as an archive module, this module must be loaded via
   <xref linkend="guc-archive-library"/>, and <xref linkend="guc-archive-mode"/>
-  must be enabled.
+  must be enabled.  For use as a restore module, this module must be loaded
+  via <xref linkend="guc-restore-library"/>, and recovery must be enabled (see
+  <xref linkend="runtime-config-wal-archive-recovery"/>).
  </para>
 
  <sect2 id="basic-archive-configuration-parameters">
@@ -34,11 +37,12 @@
     </term>
     <listitem>
      <para>
-      The directory where the server should copy WAL segment files.  This
-      directory must already exist.  The default is an empty string, which
-      effectively halts WAL archiving, but if <xref linkend="guc-archive-mode"/>
-      is enabled, the server will accumulate WAL segment files in the
-      expectation that a value will soon be provided.
+      The directory where the server should copy WAL segment files to or from.
+      This directory must already exist.  The default is an empty string,
+      which, when used for archiving, effectively halts WAL archival, but if
+      <xref linkend="guc-archive-mode"/> is enabled, the server will accumulate
+      WAL segment files in the expectation that a value will soon be provided.
+      When an empty string is used for recovery, restore will fail.
      </para>
     </listitem>
    </varlistentry>
@@ -46,7 +50,7 @@
 
   <para>
    These parameters must be set in <filename>postgresql.conf</filename>.
-   Typical usage might be:
+   Typical usage as an archive module might be:
   </para>
 
 <programlisting>
@@ -61,6 +65,7 @@ basic_archive.archive_directory = '/path/to/archive/directory'
   <title>Notes</title>
 
   <para>
+   When <filename>basic_archive</filename> is used as an archive module,
    Server crashes may leave temporary files with the prefix
    <filename>archtemp</filename> in the archive directory.  It is recommended to
    delete such files before restarting the server after a crash.  It is safe to
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 65a6e6c408..9a2570f87f 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -3906,7 +3906,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
@@ -3934,7 +3935,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,
@@ -3969,7 +3971,42 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
 
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>restore_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry id="guc-restore-library" xreflabel="restore_library">
+      <term><varname>restore_library</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>restore_library</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        The library to use for restore actions, including retrieving archived
+        files and executing tasks at restartpoints and at recovery end.  Either
+        <xref linkend="guc-restore-command"/> or
+        <varname>restore_library</varname> is required for archive recovery,
+        but optional for streaming replication.  If this parameter is set to an
+        empty string (the default), restoring via shell is enabled, and
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, and
+        <varname>recovery_end_command</varname> are used.  If both
+        <varname>restore_library</varname> and any of
+        <varname>restore_command</varname>,
+        <varname>archive_cleanup_command</varname>, or
+        <varname>recovery_end_command</varname> are set, an error will be
+        raised.  Otherwise, the specified shared library is used for restoring.
+        For more information, see <xref linkend="restore-modules"/>.
+       </para>
+
+       <para>
+        This parameter can only be set in the
+        <filename>postgresql.conf</filename> file or on the server command line.
        </para>
       </listitem>
      </varlistentry>
@@ -4014,7 +4051,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>
@@ -4043,7 +4083,10 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"'  # Windows
        </para>
        <para>
         This parameter can only be set in the <filename>postgresql.conf</filename>
-        file or on the server command line.
+        file or on the server command line.  It is only used if
+        <varname>restore_library</varname> is set to an empty string.  If both
+        <varname>recovery_end_command</varname> and
+        <varname>restore_library</varname> are set, an error will be raised.
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/high-availability.sgml b/doc/src/sgml/high-availability.sgml
index b48209fc2f..a86a9fe93a 100644
--- a/doc/src/sgml/high-availability.sgml
+++ b/doc/src/sgml/high-availability.sgml
@@ -627,7 +627,8 @@ protocol to make nodes agree on a serializable transactional order.
    <para>
     In standby mode, the server continuously applies WAL received from the
     primary server. The standby server can read WAL from a WAL archive
-    (see <xref linkend="guc-restore-command"/>) or directly from the primary
+    (see <xref linkend="guc-restore-command"/> and
+    <xref linkend="guc-restore-library"/>) or directly from the primary
     over a TCP connection (streaming replication). The standby server will
     also attempt to restore any WAL found in the standby cluster's
     <filename>pg_wal</filename> directory. That typically happens after a server
@@ -638,8 +639,10 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     At startup, the standby begins by restoring all WAL available in the
-    archive location, calling <varname>restore_command</varname>. Once it
+    archive location, either by calling <varname>restore_command</varname> or
+    by executing the <varname>restore_library</varname>'s callbacks.  Once it
     reaches the end of WAL available there and <varname>restore_command</varname>
+    or <varname>restore_library</varname>
     fails, it tries to restore any WAL available in the <filename>pg_wal</filename> directory.
     If that fails, and streaming replication has been configured, the
     standby tries to connect to the primary server and start streaming WAL
@@ -698,7 +701,8 @@ protocol to make nodes agree on a serializable transactional order.
     server (see <xref linkend="backup-pitr-recovery"/>). Create a file
     <link linkend="file-standby-signal"><filename>standby.signal</filename></link><indexterm><primary>standby.signal</primary></indexterm>
     in the standby's cluster data
-    directory. Set <xref linkend="guc-restore-command"/> to a simple command to copy files from
+    directory. Set <xref linkend="guc-restore-command"/> or
+    <xref linkend="guc-restore-library"/> to copy files from
     the WAL archive. If you plan to have multiple standby servers for high
     availability purposes, make sure that <varname>recovery_target_timeline</varname> is set to
     <literal>latest</literal> (the default), to make the standby server follow the timeline change
@@ -707,7 +711,8 @@ protocol to make nodes agree on a serializable transactional order.
 
    <note>
      <para>
-     <xref linkend="guc-restore-command"/> should return immediately
+     <xref linkend="guc-restore-command"/> and
+     <xref linkend="guc-restore-library"/> should return immediately
      if the file does not exist; the server will retry the command again if
      necessary.
     </para>
@@ -731,8 +736,9 @@ protocol to make nodes agree on a serializable transactional order.
 
    <para>
     If you're using a WAL archive, its size can be minimized using the <xref
-    linkend="guc-archive-cleanup-command"/> parameter to remove files that are no
-    longer required by the standby server.
+    linkend="guc-archive-cleanup-command"/> parameter or the
+    <xref linkend="guc-restore-library"/>'s archive cleanup callbacks to remove
+    files that are no longer required by the standby server.
     The <application>pg_archivecleanup</application> utility is designed specifically to
     be used with <varname>archive_cleanup_command</varname> in typical single-standby
     configurations, see <xref linkend="pgarchivecleanup"/>.
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c
index e5d12f1d15..34a21c40a5 100644
--- a/src/backend/access/transam/xlog.c
+++ b/src/backend/access/transam/xlog.c
@@ -83,7 +83,7 @@
 #include "replication/snapbuild.h"
 #include "replication/walreceiver.h"
 #include "replication/walsender.h"
-#include "restore/shell_restore.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
@@ -5178,18 +5178,19 @@ CleanupAfterArchiveRecovery(TimeLineID EndOfLogTLI, XLogRecPtr EndOfLog,
 							TimeLineID newTLI)
 {
 	/*
-	 * Execute the recovery_end_command, if any.
+	 * Execute the recovery-end callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during
+	 * The callback is provided the archive file cutoff point for use during
 	 * log shipping replication.  All files earlier than this point can be
 	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_recovery_end_configured())
+	if (recovery_end_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_recovery_end(lastRestartPointFname);
+		RestoreCallbacks->recovery_end_cb(restore_module_state,
+										  lastRestartPointFname);
 	}
 
 	/*
@@ -7673,18 +7674,19 @@ CreateRestartPoint(int flags)
 							   timestamptz_to_str(xtime)) : 0));
 
 	/*
-	 * Finally, execute archive_cleanup_command, if any.
+	 * Finally, execute archive-cleanup callback, if any.
 	 *
-	 * The command is provided the archive file cutoff point for use during
+	 * The callback is provided the archive file cutoff point for use during
 	 * log shipping replication.  All files earlier than this point can be
 	 * deleted from the archive, though there is no requirement to do so.
 	 */
-	if (shell_archive_cleanup_configured())
+	if (archive_cleanup_configured())
 	{
 		char		lastRestartPointFname[MAXFNAMELEN];
 
 		GetOldestRestartPointFileName(lastRestartPointFname);
-		shell_archive_cleanup(lastRestartPointFname);
+		RestoreCallbacks->archive_cleanup_cb(restore_module_state,
+											 lastRestartPointFname);
 	}
 
 	return true;
diff --git a/src/backend/access/transam/xlogarchive.c b/src/backend/access/transam/xlogarchive.c
index 5756f52124..3a05581495 100644
--- a/src/backend/access/transam/xlogarchive.c
+++ b/src/backend/access/transam/xlogarchive.c
@@ -23,14 +23,21 @@
 #include "access/xlog_internal.h"
 #include "access/xlogarchive.h"
 #include "common/archive.h"
+#include "fmgr.h"
 #include "miscadmin.h"
 #include "pgstat.h"
 #include "postmaster/pgarch.h"
 #include "postmaster/startup.h"
 #include "replication/walsender.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
+#include "utils/memutils.h"
+
+char	   *restoreLibrary = "";
+const RestoreModuleCallbacks *RestoreCallbacks = NULL;
+RestoreModuleState *restore_module_state;
 
 /*
  * Attempt to retrieve the specified file from off-line archival storage.
@@ -74,15 +81,15 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			if (!shell_restore_file_configured())
+			if (!restore_wal_segment_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			if (!shell_restore_file_configured())
+			if (!restore_timeline_history_configured())
 				goto not_available;
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			if (!shell_restore_file_configured())
+			if (!timeline_history_exists_configured())
 				goto not_available;
 			break;
 	}
@@ -174,14 +181,17 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 	switch (archive_type)
 	{
 		case ARCHIVE_TYPE_WAL_SEGMENT:
-			ret = shell_restore_wal_segment(xlogfname, xlogpath,
-											lastRestartPointFname);
+			ret = RestoreCallbacks->restore_wal_segment_cb(restore_module_state,
+														   xlogfname, xlogpath,
+														   lastRestartPointFname);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->restore_timeline_history_cb(restore_module_state,
+																xlogfname, xlogpath);
 			break;
 		case ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS:
-			ret = shell_restore_timeline_history(xlogfname, xlogpath);
+			ret = RestoreCallbacks->timeline_history_exists_cb(restore_module_state,
+															   xlogfname, xlogpath);
 			break;
 	}
 
@@ -189,6 +199,16 @@ RestoreArchivedFile(char *path, const char *xlogfname,
 
 	if (ret)
 	{
+		/*
+		 * Some restore functions might not copy the file (e.g., checking
+		 * whether a timeline history file exists), but they can set a flag to
+		 * tell us if they do.  We only need to verify file existence if this
+		 * flag is enabled.
+		 */
+		if (archive_type == ARCHIVE_TYPE_TIMELINE_HISTORY_EXISTS &&
+			!restore_module_state->timeline_history_exists_cb_copies)
+			return true;
+
 		/*
 		 * command apparently succeeded, but let's make sure the file is
 		 * really there now and has the correct size.
@@ -630,3 +650,65 @@ XLogArchiveCleanup(const char *xlog)
 	unlink(archiveStatusPath);
 	/* should we complain about failure? */
 }
+
+/*
+ * Loads all the restore callbacks into our global RestoreCallbacks.  The
+ * caller is responsible for validating the combination of library/command
+ * parameters that are set (e.g., restore_command and restore_library cannot
+ * both be set).
+ */
+void
+LoadRestoreCallbacks(void)
+{
+	RestoreModuleInit init;
+
+	/*
+	 * If the shell command is enabled, use our special initialization
+	 * function.  Otherwise, load the library and call its
+	 * _PG_restore_module_init().
+	 */
+	if (restoreLibrary[0] == '\0')
+		init = shell_restore_init;
+	else
+		init = (RestoreModuleInit)
+			load_external_function(restoreLibrary, "_PG_restore_module_init",
+								   false, NULL);
+
+	if (init == NULL)
+		ereport(ERROR,
+				(errmsg("restore modules have to define the symbol "
+						"_PG_restore_module_init")));
+
+	RestoreCallbacks = (*init) ();
+
+	/* restore state should be freed before calling this function */
+	Assert(restore_module_state == NULL);
+	restore_module_state = (RestoreModuleState *)
+		MemoryContextAllocZero(TopMemoryContext,
+							   sizeof(RestoreModuleState));
+
+	if (RestoreCallbacks->startup_cb != NULL)
+		RestoreCallbacks->startup_cb(restore_module_state);
+}
+
+/*
+ * Call the shutdown callback of the loaded restore module, if defined.  Also,
+ * free the restore module state if it was allocated.
+ *
+ * Processes that load restore libraries should register this via
+ * before_shmem_exit().
+ */
+void
+call_restore_module_shutdown_cb(int code, Datum arg)
+{
+	if (RestoreCallbacks != NULL &&
+		RestoreCallbacks->shutdown_cb != NULL &&
+		restore_module_state != NULL)
+		RestoreCallbacks->shutdown_cb(restore_module_state);
+
+	if (restore_module_state != NULL)
+	{
+		pfree(restore_module_state);
+		restore_module_state = NULL;
+	}
+}
diff --git a/src/backend/access/transam/xlogrecovery.c b/src/backend/access/transam/xlogrecovery.c
index 07213b1897..1bf0d0ea11 100644
--- a/src/backend/access/transam/xlogrecovery.c
+++ b/src/backend/access/transam/xlogrecovery.c
@@ -51,6 +51,7 @@
 #include "replication/slot.h"
 #include "replication/slotsync.h"
 #include "replication/walreceiver.h"
+#include "restore/restore_module.h"
 #include "storage/fd.h"
 #include "storage/ipc.h"
 #include "storage/latch.h"
@@ -1117,18 +1118,21 @@ validateRecoveryParameters(void)
 	if (StandbyModeRequested)
 	{
 		if ((PrimaryConnInfo == NULL || strcmp(PrimaryConnInfo, "") == 0) &&
-			(recoveryRestoreCommand == NULL || strcmp(recoveryRestoreCommand, "") == 0))
+			(!restore_wal_segment_configured() ||
+			 !restore_timeline_history_configured() ||
+			 !timeline_history_exists_configured()))
 			ereport(WARNING,
-					(errmsg("specified neither primary_conninfo nor restore_command"),
+					(errmsg("specified neither primary_conninfo nor restore_command nor a configured restore_library"),
 					 errhint("The database server will regularly poll the pg_wal subdirectory to check for files placed there.")));
 	}
 	else
 	{
-		if (recoveryRestoreCommand == NULL ||
-			strcmp(recoveryRestoreCommand, "") == 0)
+		if (!restore_wal_segment_configured() ||
+			!restore_timeline_history_configured() ||
+			!timeline_history_exists_configured())
 			ereport(FATAL,
 					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-					 errmsg("must specify restore_command when standby mode is not enabled")));
+					 errmsg("must specify restore_command or a configured restore_library when standby mode is not enabled")));
 	}
 
 	/*
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 8ef600ae72..c31d0f3bd2 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -46,6 +46,7 @@
 #include "postmaster/bgwriter.h"
 #include "postmaster/interrupt.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/condition_variable.h"
 #include "storage/fd.h"
@@ -230,6 +231,25 @@ CheckpointerMain(char *startup_data, size_t startup_data_len)
 												 ALLOCSET_DEFAULT_SIZES);
 	MemoryContextSwitchTo(checkpointer_context);
 
+	/*
+	 * Load restore_library so that we can use its archive-cleanup callback,
+	 * if one is defined.
+	 *
+	 * We also take this opportunity to set up the before_shmem_exit hook for
+	 * the shutdown callback and to check that only one of restore_library,
+	 * archive_cleanup_command is set.  Note that we emit a WARNING upon
+	 * detecting misconfigured parameters, and we clear the callbacks so that
+	 * the archive-cleanup functionality is skipped.  We judge this
+	 * functionality to not be important enough to require blocking
+	 * checkpoints or shutting down the server when the parameters are
+	 * misconfigured.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+										 archiveCleanupCommand, "archive_cleanup_command",
+										 WARNING))
+		LoadRestoreCallbacks();
+
 	/*
 	 * If an exception is encountered, processing resumes here.
 	 *
@@ -562,6 +582,10 @@ HandleCheckpointerInterrupts(void)
 
 	if (ConfigReloadPending)
 	{
+		bool		archive_cleanup_settings_changed = false;
+		char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+		char	   *prevArchiveCleanupCommand = pstrdup(archiveCleanupCommand);
+
 		ConfigReloadPending = false;
 		ProcessConfigFile(PGC_SIGHUP);
 
@@ -577,6 +601,36 @@ HandleCheckpointerInterrupts(void)
 		 * because of SIGHUP.
 		 */
 		UpdateSharedMemoryConfig();
+
+		/*
+		 * If the archive-cleanup settings have changed, call the currently
+		 * loaded shutdown callback and clear all the restore callbacks.
+		 * There's presently no good way to unload a library besides
+		 * restarting the process, and there should be little harm in leaving
+		 * it around, so we just leave it loaded.
+		 */
+		if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+			strcmp(prevArchiveCleanupCommand, archiveCleanupCommand) != 0)
+		{
+			archive_cleanup_settings_changed = true;
+			call_restore_module_shutdown_cb(0, (Datum) 0);
+			RestoreCallbacks = NULL;
+		}
+
+		/*
+		 * As in CheckpointerMain(), we only emit a WARNING if we detect that
+		 * both restore_library and archive_cleanup_command are set.  We do
+		 * this even if the archive-cleanup settings haven't changed to remind
+		 * the user about the misconfiguration.
+		 */
+		if (CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											 archiveCleanupCommand, "archive_cleanup_command",
+											 WARNING) &&
+			archive_cleanup_settings_changed)
+			LoadRestoreCallbacks();
+
+		pfree(prevRestoreLibrary);
+		pfree(prevArchiveCleanupCommand);
 	}
 	if (ShutdownRequestPending)
 	{
diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c
index cc665ce259..f8d3d6b3d2 100644
--- a/src/backend/postmaster/startup.c
+++ b/src/backend/postmaster/startup.c
@@ -26,6 +26,7 @@
 #include "miscadmin.h"
 #include "postmaster/auxprocess.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "storage/ipc.h"
 #include "storage/pmsignal.h"
 #include "storage/procsignal.h"
@@ -119,13 +120,17 @@ StartupProcShutdownHandler(SIGNAL_ARGS)
  * Re-read the config file.
  *
  * If one of the critical walreceiver options has changed, flag xlog.c
- * to restart it.
+ * to restart it.  Also, check for invalid combinations of the command/library
+ * parameters and reload the restore callbacks if necessary.
  */
 static void
 StartupRereadConfig(void)
 {
 	char	   *conninfo = pstrdup(PrimaryConnInfo);
 	char	   *slotname = pstrdup(PrimarySlotName);
+	char	   *prevRestoreLibrary = pstrdup(restoreLibrary);
+	char	   *prevRestoreCommand = pstrdup(recoveryRestoreCommand);
+	char	   *prevRecoveryEndCommand = pstrdup(recoveryEndCommand);
 	bool		tempSlot = wal_receiver_create_temp_slot;
 	bool		conninfoChanged;
 	bool		slotnameChanged;
@@ -147,6 +152,30 @@ StartupRereadConfig(void)
 
 	if (conninfoChanged || slotnameChanged || tempSlotChanged)
 		StartupRequestWalReceiverRestart();
+
+	/*
+	 * If the restore settings have changed, call the currently loaded
+	 * shutdown callback and load the new callbacks.  There's presently no
+	 * good way to unload a library besides restarting the process, and there
+	 * should be little harm in leaving it around, so we just leave it loaded.
+	 */
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	if (strcmp(prevRestoreLibrary, restoreLibrary) != 0 ||
+		strcmp(prevRestoreCommand, recoveryRestoreCommand) != 0 ||
+		strcmp(prevRecoveryEndCommand, recoveryEndCommand) != 0)
+	{
+		call_restore_module_shutdown_cb(0, (Datum) 0);
+		LoadRestoreCallbacks();
+	}
+
+	pfree(prevRestoreLibrary);
+	pfree(prevRestoreCommand);
+	pfree(prevRecoveryEndCommand);
 }
 
 /* Handle various signals that might be sent to the startup process */
@@ -261,6 +290,19 @@ StartupProcessMain(char *startup_data, size_t startup_data_len)
 	 */
 	sigprocmask(SIG_SETMASK, &UnBlockSig, NULL);
 
+	/*
+	 * Check for invalid combinations of the command/library parameters and
+	 * load the callbacks.
+	 */
+	before_shmem_exit(call_restore_module_shutdown_cb, 0);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryRestoreCommand, "restore_command",
+											ERROR);
+	(void) CheckMutuallyExclusiveStringGUCs(restoreLibrary, "restore_library",
+											recoveryEndCommand, "recovery_end_command",
+											ERROR);
+	LoadRestoreCallbacks();
+
 	/*
 	 * Do what we came for.
 	 */
diff --git a/src/backend/restore/shell_restore.c b/src/backend/restore/shell_restore.c
index 42291625c1..be25f04044 100644
--- a/src/backend/restore/shell_restore.c
+++ b/src/backend/restore/shell_restore.c
@@ -3,7 +3,8 @@
  * shell_restore.c
  *
  * These restore functions use a user-specified shell command (e.g., the
- * restore_command GUC).
+ * restore_command GUC).  It is used as the default, but other modules may
+ * define their own restore logic.
  *
  * Copyright (c) 2024, PostgreSQL Global Development Group
  *
@@ -22,25 +23,76 @@
 #include "common/archive.h"
 #include "common/percentrepl.h"
 #include "postmaster/startup.h"
+#include "restore/restore_module.h"
 #include "restore/shell_restore.h"
 #include "storage/ipc.h"
 #include "utils/wait_event.h"
 
+static void shell_restore_startup(RestoreModuleState *state);
+static bool shell_restore_file_configured(RestoreModuleState *state);
 static bool shell_restore_file(const char *file, const char *path,
 							   const char *lastRestartPointFileName);
+static bool shell_restore_wal_segment(RestoreModuleState *state,
+									  const char *file, const char *path,
+									  const char *lastRestartPointFileName);
+static bool shell_restore_timeline_history(RestoreModuleState *state,
+										   const char *file, const char *path);
+static bool shell_archive_cleanup_configured(RestoreModuleState *state);
+static void shell_archive_cleanup(RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+static bool shell_recovery_end_configured(RestoreModuleState *state);
+static void shell_recovery_end(RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
 static void ExecuteRecoveryCommand(const char *command,
 								   const char *commandName,
 								   bool failOnSignal,
 								   uint32 wait_event_info,
 								   const char *lastRestartPointFileName);
 
+static const RestoreModuleCallbacks shell_restore_callbacks = {
+	.startup_cb = shell_restore_startup,
+	.restore_wal_segment_configured_cb = shell_restore_file_configured,
+	.restore_wal_segment_cb = shell_restore_wal_segment,
+	.restore_timeline_history_configured_cb = shell_restore_file_configured,
+	.restore_timeline_history_cb = shell_restore_timeline_history,
+	.timeline_history_exists_configured_cb = shell_restore_file_configured,
+	.timeline_history_exists_cb = shell_restore_timeline_history,
+	.archive_cleanup_configured_cb = shell_archive_cleanup_configured,
+	.archive_cleanup_cb = shell_archive_cleanup,
+	.recovery_end_configured_cb = shell_recovery_end_configured,
+	.recovery_end_cb = shell_recovery_end,
+	.shutdown_cb = NULL
+};
+
+/*
+ * shell_restore_init
+ *
+ * Returns the callbacks for restoring via shell.
+ */
+const RestoreModuleCallbacks *
+shell_restore_init(void)
+{
+	return &shell_restore_callbacks;
+}
+
+/*
+ * Indicates that our timeline_history_exists_cb only attempts to retrieve the
+ * file, so the caller should verify the file exists afterwards.
+ */
+static void
+shell_restore_startup(RestoreModuleState *state)
+{
+	state->timeline_history_exists_cb_copies = true;
+}
+
 /*
  * Attempt to execute a shell-based restore command to retrieve a WAL segment.
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_wal_segment(const char *file, const char *path,
+static bool
+shell_restore_wal_segment(RestoreModuleState *state,
+						  const char *file, const char *path,
 						  const char *lastRestartPointFileName)
 {
 	return shell_restore_file(file, path, lastRestartPointFileName);
@@ -52,8 +104,9 @@ shell_restore_wal_segment(const char *file, const char *path,
  *
  * Returns true if the command has succeeded, false otherwise.
  */
-bool
-shell_restore_timeline_history(const char *file, const char *path)
+static bool
+shell_restore_timeline_history(RestoreModuleState *state,
+							   const char *file, const char *path)
 {
 	char		lastRestartPointFname[MAXPGPATH];
 
@@ -64,8 +117,8 @@ shell_restore_timeline_history(const char *file, const char *path)
 /*
  * Check whether restore_command is supplied.
  */
-bool
-shell_restore_file_configured(void)
+static bool
+shell_restore_file_configured(RestoreModuleState *state)
 {
 	return recoveryRestoreCommand[0] != '\0';
 }
@@ -152,8 +205,8 @@ shell_restore_file(const char *file, const char *path,
 /*
  * Check whether archive_cleanup_command is supplied.
  */
-bool
-shell_archive_cleanup_configured(void)
+static bool
+shell_archive_cleanup_configured(RestoreModuleState *state)
 {
 	return archiveCleanupCommand[0] != '\0';
 }
@@ -161,8 +214,9 @@ shell_archive_cleanup_configured(void)
 /*
  * Attempt to execute a shell-based archive cleanup command.
  */
-void
-shell_archive_cleanup(const char *lastRestartPointFileName)
+static void
+shell_archive_cleanup(RestoreModuleState *state,
+					  const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(archiveCleanupCommand, "archive_cleanup_command",
 						   false, WAIT_EVENT_ARCHIVE_CLEANUP_COMMAND,
@@ -172,8 +226,8 @@ shell_archive_cleanup(const char *lastRestartPointFileName)
 /*
  * Check whether recovery_end_command is supplied.
  */
-bool
-shell_recovery_end_configured(void)
+static bool
+shell_recovery_end_configured(RestoreModuleState *state)
 {
 	return recoveryEndCommand[0] != '\0';
 }
@@ -181,8 +235,9 @@ shell_recovery_end_configured(void)
 /*
  * Attempt to execute a shell-based end-of-recovery command.
  */
-void
-shell_recovery_end(const char *lastRestartPointFileName)
+static void
+shell_recovery_end(RestoreModuleState *state,
+				   const char *lastRestartPointFileName)
 {
 	ExecuteRecoveryCommand(recoveryEndCommand, "recovery_end_command", true,
 						   WAIT_EVENT_RECOVERY_END_COMMAND,
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 1e71e7db4a..f84724a0ed 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -70,6 +70,7 @@
 #include "replication/slot.h"
 #include "replication/slotsync.h"
 #include "replication/syncrep.h"
+#include "restore/restore_module.h"
 #include "storage/bufmgr.h"
 #include "storage/large_object.h"
 #include "storage/pg_shmem.h"
@@ -3946,6 +3947,16 @@ struct config_string ConfigureNamesString[] =
 		NULL, NULL, NULL
 	},
 
+	{
+		{"restore_library", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
+			gettext_noop("Sets the library that will be called for restore actions."),
+			NULL
+		},
+		&restoreLibrary,
+		"",
+		NULL, NULL, NULL
+	},
+
 	{
 		{"archive_cleanup_command", PGC_SIGHUP, WAL_ARCHIVE_RECOVERY,
 			gettext_noop("Sets the shell command that will be executed at every restart point."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 2244ee52f7..5265857519 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -284,6 +284,7 @@
 				# placeholders: %p = path of file to restore
 				#               %f = file name only
 				# e.g. 'cp /mnt/server/archivedir/%f %p'
+#restore_library = ''		# library to use for restore actions
 #archive_cleanup_command = ''	# command to execute at every restartpoint
 #recovery_end_command = ''	# command to execute at completion of recovery
 
diff --git a/src/include/restore/restore_module.h b/src/include/restore/restore_module.h
new file mode 100644
index 0000000000..cc148e855a
--- /dev/null
+++ b/src/include/restore/restore_module.h
@@ -0,0 +1,143 @@
+/*-------------------------------------------------------------------------
+ *
+ * restore_module.h
+ *		Exports for restore modules.
+ *
+ * Copyright (c) 2024, PostgreSQL Global Development Group
+ *
+ * src/include/restore/restore_module.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef _RESTORE_MODULE_H
+#define _RESTORE_MODULE_H
+
+/*
+ * The value of the restore_library GUC.
+ */
+extern PGDLLIMPORT char *restoreLibrary;
+
+typedef struct RestoreModuleState
+{
+	/*
+	 * Private data pointer for use by a restore module.  This can be used to
+	 * store state for the module that will be passed to each of its
+	 * callbacks.
+	 */
+	void	   *private_data;
+
+	/*
+	 * Indicates whether the callback that checks for timeline existence
+	 * merely copies the file.  If set to true, the server will verify that
+	 * the timeline history file exists after the callback returns.
+	 */
+	bool		timeline_history_exists_cb_copies;
+} RestoreModuleState;
+
+/*
+ * Restore module callbacks
+ *
+ * These callback functions should be defined by restore libraries and returned
+ * via _PG_restore_module_init().  For more information about the purpose of
+ * each callback, refer to the restore modules documentation.
+ */
+typedef void (*RestoreStartupCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreWalSegmentCB) (RestoreModuleState *state,
+									 const char *file, const char *path,
+									 const char *lastRestartPointFileName);
+typedef bool (*RestoreTimelineHistoryConfiguredCB) (RestoreModuleState *state);
+typedef bool (*RestoreTimelineHistoryCB) (RestoreModuleState *state,
+										  const char *file, const char *path);
+typedef bool (*TimelineHistoryExistsConfiguredCB) (RestoreModuleState *state);
+typedef bool (*TimelineHistoryExistsCB) (RestoreModuleState *state,
+										 const char *file, const char *path);
+typedef bool (*ArchiveCleanupConfiguredCB) (RestoreModuleState *state);
+typedef void (*ArchiveCleanupCB) (RestoreModuleState *state,
+								  const char *lastRestartPointFileName);
+typedef bool (*RecoveryEndConfiguredCB) (RestoreModuleState *state);
+typedef void (*RecoveryEndCB) (RestoreModuleState *state,
+							   const char *lastRestartPointFileName);
+typedef void (*RestoreShutdownCB) (RestoreModuleState *state);
+
+typedef struct RestoreModuleCallbacks
+{
+	RestoreStartupCB startup_cb;
+	RestoreWalSegmentConfiguredCB restore_wal_segment_configured_cb;
+	RestoreWalSegmentCB restore_wal_segment_cb;
+	RestoreTimelineHistoryConfiguredCB restore_timeline_history_configured_cb;
+	RestoreTimelineHistoryCB restore_timeline_history_cb;
+	TimelineHistoryExistsConfiguredCB timeline_history_exists_configured_cb;
+	TimelineHistoryExistsCB timeline_history_exists_cb;
+	ArchiveCleanupConfiguredCB archive_cleanup_configured_cb;
+	ArchiveCleanupCB archive_cleanup_cb;
+	RecoveryEndConfiguredCB recovery_end_configured_cb;
+	RecoveryEndCB recovery_end_cb;
+	RestoreShutdownCB shutdown_cb;
+} RestoreModuleCallbacks;
+
+/*
+ * Type of the shared library symbol _PG_restore_module_init that is looked up
+ * when loading a restore library.
+ */
+typedef const RestoreModuleCallbacks *(*RestoreModuleInit) (void);
+
+extern PGDLLEXPORT const RestoreModuleCallbacks *_PG_restore_module_init(void);
+
+extern void LoadRestoreCallbacks(void);
+extern void call_restore_module_shutdown_cb(int code, Datum arg);
+
+extern const RestoreModuleCallbacks *RestoreCallbacks;
+extern RestoreModuleState *restore_module_state;
+
+/*
+ * The following *_configured() functions are convenient wrappers around the
+ * *_configured_cb() callback functions with additional defensive NULL checks.
+ */
+
+static inline bool
+restore_wal_segment_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->restore_wal_segment_configured_cb != NULL &&
+		RestoreCallbacks->restore_wal_segment_cb != NULL &&
+		RestoreCallbacks->restore_wal_segment_configured_cb(restore_module_state);
+}
+
+static inline bool
+restore_timeline_history_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->restore_timeline_history_configured_cb != NULL &&
+		RestoreCallbacks->restore_timeline_history_cb != NULL &&
+		RestoreCallbacks->restore_timeline_history_configured_cb(restore_module_state);
+}
+
+static inline bool
+timeline_history_exists_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->timeline_history_exists_configured_cb != NULL &&
+		RestoreCallbacks->timeline_history_exists_cb != NULL &&
+		RestoreCallbacks->timeline_history_exists_configured_cb(restore_module_state);
+}
+
+static inline bool
+archive_cleanup_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->archive_cleanup_configured_cb != NULL &&
+		RestoreCallbacks->archive_cleanup_cb != NULL &&
+		RestoreCallbacks->archive_cleanup_configured_cb(restore_module_state);
+}
+
+static inline bool
+recovery_end_configured(void)
+{
+	return RestoreCallbacks != NULL &&
+		RestoreCallbacks->recovery_end_configured_cb != NULL &&
+		RestoreCallbacks->recovery_end_cb != NULL &&
+		RestoreCallbacks->recovery_end_configured_cb(restore_module_state);
+}
+
+#endif							/* _RESTORE_MODULE_H */
diff --git a/src/include/restore/shell_restore.h b/src/include/restore/shell_restore.h
index 28b5b7bc92..6352623866 100644
--- a/src/include/restore/shell_restore.h
+++ b/src/include/restore/shell_restore.h
@@ -12,15 +12,13 @@
 #ifndef _SHELL_RESTORE_H
 #define _SHELL_RESTORE_H
 
-extern bool shell_restore_file_configured(void);
-extern bool shell_restore_wal_segment(const char *file, const char *path,
-									  const char *lastRestartPointFileName);
-extern bool shell_restore_timeline_history(const char *file, const char *path);
+#include "restore/restore_module.h"
 
-extern bool shell_archive_cleanup_configured(void);
-extern void shell_archive_cleanup(const char *lastRestartPointFileName);
-
-extern bool shell_recovery_end_configured(void);
-extern void shell_recovery_end(const char *lastRestartPointFileName);
+/*
+ * Since the logic for restoring via shell commands is in the core server and
+ * does not need to be loaded via a shared library, it has a special
+ * initialization function.
+ */
+extern const RestoreModuleCallbacks *shell_restore_init(void);
 
 #endif							/* _SHELL_RESTORE_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a40c59b2e0..db4cc68a88 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2405,6 +2405,8 @@ ResourceOwnerDesc
 ResourceReleaseCallback
 ResourceReleaseCallbackItem
 ResourceReleasePhase
+RestoreModuleCallbacks
+RestoreModuleState
 RestoreOptions
 RestorePass
 RestrictInfo
-- 
2.25.1


--3V7upXqbjpZ4EhLz--





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

* Re: Simplify if/else logic of walsender CreateReplicationSlot
@ 2023-11-21 04:57  Michael Paquier <[email protected]>
  0 siblings, 1 reply; 28+ messages in thread

From: Michael Paquier @ 2023-11-21 04:57 UTC (permalink / raw)
  To: Peter Smith <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>

On Mon, Nov 20, 2023 at 05:07:38PM +0900, Michael Paquier wrote:
> Good idea.  What you are suggesting here improves the readability of
> this code, so +1.

And applied this one, thanks!
--
Michael


Attachments:

  [application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
  download

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

* Re: Simplify if/else logic of walsender CreateReplicationSlot
@ 2023-11-21 05:52  Peter Smith <[email protected]>
  parent: Michael Paquier <[email protected]>
  0 siblings, 0 replies; 28+ messages in thread

From: Peter Smith @ 2023-11-21 05:52 UTC (permalink / raw)
  To: Michael Paquier <[email protected]>; +Cc: PostgreSQL Hackers <[email protected]>

On Tue, Nov 21, 2023 at 3:57 PM Michael Paquier <[email protected]> wrote:
>
> On Mon, Nov 20, 2023 at 05:07:38PM +0900, Michael Paquier wrote:
> > Good idea.  What you are suggesting here improves the readability of
> > this code, so +1.
>
> And applied this one, thanks!

Thanks for pushing.

======
Kind Regards,
Peter Smith.
Fujitsu Australia






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


end of thread, other threads:[~2023-11-21 05:52 UTC | newest]

Thread overview: 28+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2018-06-14 05:28 question on streaming replication Atul Kumar <[email protected]>
2018-06-14 12:14 ` Fabio Pardi <[email protected]>
2018-06-15 14:36 ` Amit Kapila <[email protected]>
2018-06-15 16:43 ` Andreas Kretschmer <[email protected]>
2018-12-11 14:52 Re: Introducing SNI in TLS handshake for SSL connections Pablo Iranzo Gómez <[email protected]>
2018-12-11 17:18 ` Re: Introducing SNI in TLS handshake for SSL connections Andreas Karlsson <[email protected]>
2018-12-12 20:46   ` Re: Introducing SNI in TLS handshake for SSL connections Pablo Iranzo Gómez <[email protected]>
2018-12-13 00:30 ` Re: Introducing SNI in TLS handshake for SSL connections Andreas Karlsson <[email protected]>
2018-12-13 06:43   ` Re: Introducing SNI in TLS handshake for SSL connections Pablo Iranzo Gómez <[email protected]>
2018-12-13 13:07     ` Re: Introducing SNI in TLS handshake for SSL connections Andreas Karlsson <[email protected]>
2018-12-14 01:30       ` Re: Introducing SNI in TLS handshake for SSL connections Chapman Flack <[email protected]>
2018-12-14 07:37   ` Re: Introducing SNI in TLS handshake for SSL connections Pablo Iranzo Gómez <[email protected]>
2023-01-23 19:47 [PATCH v10 4/4] introduce restore_library Nathan Bossart <[email protected]>
2023-02-16 06:06 [PATCH v14 7/7] introduce restore_library Nathan Bossart <[email protected]>
2023-02-16 06:06 [PATCH v18 5/5] introduce restore_library Nathan Bossart <[email protected]>
2023-02-16 06:06 [PATCH v16 5/5] introduce restore_library Nathan Bossart <[email protected]>
2023-02-16 06:06 [PATCH v19 5/5] introduce restore_library Nathan Bossart <[email protected]>
2023-02-16 06:06 [PATCH v13 7/7] introduce restore_library Nathan Bossart <[email protected]>
2023-02-16 06:06 [PATCH v12 6/6] introduce restore_library Nathan Bossart <[email protected]>
2023-02-16 06:06 [PATCH v20 5/5] introduce restore_library Nathan Bossart <[email protected]>
2023-02-16 06:06 [PATCH v19 5/5] introduce restore_library Nathan Bossart <[email protected]>
2023-02-16 06:06 [PATCH v20 5/5] introduce restore_library Nathan Bossart <[email protected]>
2023-02-16 06:06 [PATCH v15 7/7] introduce restore_library Nathan Bossart <[email protected]>
2023-02-16 06:06 [PATCH v18 5/5] introduce restore_library Nathan Bossart <[email protected]>
2023-02-16 06:06 [PATCH v18 5/5] introduce restore_library Nathan Bossart <[email protected]>
2023-02-16 06:06 [PATCH v19 5/5] introduce restore_library Nathan Bossart <[email protected]>
2023-11-21 04:57 Re: Simplify if/else logic of walsender CreateReplicationSlot Michael Paquier <[email protected]>
2023-11-21 05:52 ` Re: Simplify if/else logic of walsender CreateReplicationSlot Peter Smith <[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