public inbox for [email protected]
help / color / mirror / Atom feedRe: partition tree inspection functions
58+ messages / 12 participants
[nested] [flat]
* Re: partition tree inspection functions
@ 2018-10-01 06:03 Michael Paquier <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Michael Paquier @ 2018-10-01 06:03 UTC (permalink / raw)
To: Amit Langote <[email protected]>; +Cc: Thomas Munro <[email protected]>; Jesper Pedersen <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On Thu, Aug 09, 2018 at 01:05:56PM +0900, Amit Langote wrote:
> Attached updated patch.
So, except if I am missing something, what we have here is a patch which
has been debatted quite a bit and has semantics which look nice. Any
objections if we move forward with this patch?
+-- all tables in the tree
+select *, pg_relation_size(relid) as size from
pg_partition_children('ptif_test');
+ relid | parentid | level | isleaf | size
+-------------+------------+-------+--------+-------
+ ptif_test | | 0 | f | 0
+ ptif_test0 | ptif_test | 1 | f | 0
+ ptif_test1 | ptif_test | 1 | f | 0
+ ptif_test2 | ptif_test | 1 | t | 16384
+ ptif_test01 | ptif_test0 | 2 | t | 24576
One thing is that this test depends on the page size. There are already
plan modifications if running the regress tests with a size other than
8kB, but I don't think that we should make that worse, so I would
suggest to replace to use "pg_relation_size(relid) > 0" instead.
I have moved the patch to next CF for now.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-01 06:16 Amit Langote <[email protected]>
parent: Michael Paquier <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Amit Langote @ 2018-10-01 06:16 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: Thomas Munro <[email protected]>; Jesper Pedersen <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
Hi,
On 2018/10/01 15:03, Michael Paquier wrote:
> On Thu, Aug 09, 2018 at 01:05:56PM +0900, Amit Langote wrote:
>> Attached updated patch.
>
> So, except if I am missing something, what we have here is a patch which
> has been debatted quite a bit and has semantics which look nice.
Thanks.
> Any
> objections if we move forward with this patch?
I wasn't able to respond to some of issues that Jesper brought up with the
approach taken by the latest patch whereby there is no separate
pg_partition_level function. He said that such a function would be useful
to get the information about the individual leaf partitions, but I was no
longer sure of providing such a function separately.
> +-- all tables in the tree
> +select *, pg_relation_size(relid) as size from
> pg_partition_children('ptif_test');
> + relid | parentid | level | isleaf | size
> +-------------+------------+-------+--------+-------
> + ptif_test | | 0 | f | 0
> + ptif_test0 | ptif_test | 1 | f | 0
> + ptif_test1 | ptif_test | 1 | f | 0
> + ptif_test2 | ptif_test | 1 | t | 16384
> + ptif_test01 | ptif_test0 | 2 | t | 24576
>
> One thing is that this test depends on the page size. There are already
> plan modifications if running the regress tests with a size other than
> 8kB, but I don't think that we should make that worse, so I would
> suggest to replace to use "pg_relation_size(relid) > 0" instead.
Might be a good idea, will do.
> I have moved the patch to next CF for now.
Thank you, I'll submit an updated version soon.
Regards,
Amit
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-01 06:27 Michael Paquier <[email protected]>
parent: Amit Langote <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Michael Paquier @ 2018-10-01 06:27 UTC (permalink / raw)
To: Amit Langote <[email protected]>; +Cc: Thomas Munro <[email protected]>; Jesper Pedersen <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On Mon, Oct 01, 2018 at 03:16:32PM +0900, Amit Langote wrote:
> I wasn't able to respond to some of issues that Jesper brought up with the
> approach taken by the latest patch whereby there is no separate
> pg_partition_level function. He said that such a function would be useful
> to get the information about the individual leaf partitions, but I was no
> longer sure of providing such a function separately.
Perhaps that could be debated separately as well? From what I can see
what's available would unlock the psql patch which would like to add
support for \dP, or show the size of partitions more easily. I am also
not completely sure that I see the use-case for pg_partition_level or
even pg_partition_root_parent as usually in their schemas users append
rather similar relation names to the parent and the children. Or
perhaps not?
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-01 07:27 Amit Langote <[email protected]>
parent: Michael Paquier <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Amit Langote @ 2018-10-01 07:27 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: Thomas Munro <[email protected]>; Jesper Pedersen <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On 2018/10/01 15:27, Michael Paquier wrote:
> On Mon, Oct 01, 2018 at 03:16:32PM +0900, Amit Langote wrote:
>> I wasn't able to respond to some of issues that Jesper brought up with the
>> approach taken by the latest patch whereby there is no separate
>> pg_partition_level function. He said that such a function would be useful
>> to get the information about the individual leaf partitions, but I was no
>> longer sure of providing such a function separately.
>
> Perhaps that could be debated separately as well? From what I can see
> what's available would unlock the psql patch which would like to add
> support for \dP, or show the size of partitions more easily.
Yeah, maybe there is no reason to delay proceeding with
pg_partition_children which provides a useful functionality.
> I am also
> not completely sure that I see the use-case for pg_partition_level or
> even pg_partition_root_parent as usually in their schemas users append
> rather similar relation names to the parent and the children. Or
> perhaps not?
We can continue discussing that once we're done dealing with
pg_partition_children and then some other patches that are pending due to
it such as Pavel's.
Thanks,
Amit
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-03 03:37 Michael Paquier <[email protected]>
parent: Amit Langote <[email protected]>
0 siblings, 3 replies; 58+ messages in thread
From: Michael Paquier @ 2018-10-03 03:37 UTC (permalink / raw)
To: Amit Langote <[email protected]>; +Cc: Thomas Munro <[email protected]>; Jesper Pedersen <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On Mon, Oct 01, 2018 at 04:27:57PM +0900, Amit Langote wrote:
> Yeah, maybe there is no reason to delay proceeding with
> pg_partition_children which provides a useful functionality.
So, I have been looking at your patch, and there are a couple of things
which could be improved.
Putting the new function pg_partition_children() in partition.c is a
bad idea I think. So instead I think that we should put that in a
different location, say utils/adt/partitionfuncs.c.
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "relid",
+ REGCLASSOID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "parentid",
+ REGCLASSOID, -1, 0);
REGCLASSOID is used mainly for casting, so instead let's use OIDOID like
any other system function.
+ values[2] = psprintf("%d", level);
+ values[3] = psprintf("%c", relkind == RELKIND_PARTITIONED_TABLE ?
+ 'f' :
+ 't');
Using Datum objects is more elegant in this context.
isleaf is not particularly useful as it can just be fetched with a join
on pg_class/relkind. So I think that we had better remove it.
I have cleaned up a bit tests, removing duplicates and most of the
things which touched the size of relations to have something more
portable.
We could have a flavor using a relation name in input with qualified
names handled properly (see pg_get_viewdef_name for example), not sure
if that's really mandatory so I left that out. I have also added some
comments here and there. The docs could be worded a bit better still.
My result is the patch attached. What do you think?
--
Michael
Attachments:
[text/x-diff] partition-tree-func.patch (11.5K, ../../[email protected]/2-partition-tree-func.patch)
download | inline diff:
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 9a7f683658..d41c09b68b 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -20197,6 +20197,46 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
The function returns the number of new collation objects it created.
</para>
+ <table id="functions-info-partition">
+ <title>Partitioning Information Functions</title>
+ <tgroup cols="3">
+ <thead>
+ <row><entry>Name</entry> <entry>Return Type</entry> <entry>Description</entry></row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry><literal><function>pg_partition_tree(<type>oid</type>)</function></literal></entry>
+ <entry><type>setof record</type></entry>
+ <entry>
+ List information about a partition tree for the given partitioned
+ table, consisting of one row for each partition in a tree. The
+ information available is the OID of the partition, the OID of its
+ immediate partitioned table, and its level in the hierarchy,
+ beginning at <literal>0</literal> for the top-most parent, and
+ incremented by <literal>1</literal> for each level up.
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ <para>
+ To check the total size of the data contained in
+ <structname>measurement</structname> table described in
+ <xref linkend="ddl-partitioning-declarative-example"/>, one could use the
+ following query:
+ </para>
+
+<programlisting>
+=# SELECT pg_size_pretty(sum(pg_relation_size(relid))) AS total_size
+ FROM pg_partition_tree('measurement');
+ total_size
+------------
+ 24 kB
+(1 row)
+</programlisting>
+
</sect2>
<sect2 id="functions-admin-index">
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 4b35dbb8bb..132ec7620c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -20,8 +20,8 @@ OBJS = acl.o amutils.o arrayfuncs.o array_expanded.o array_selfuncs.o \
jsonfuncs.o like.o lockfuncs.o mac.o mac8.o misc.o nabstime.o name.o \
network.o network_gist.o network_selfuncs.o network_spgist.o \
numeric.o numutils.o oid.o oracle_compat.o \
- orderedsetaggs.o pg_locale.o pg_lsn.o pg_upgrade_support.o \
- pgstatfuncs.o \
+ orderedsetaggs.o partitionfuncs.o pg_locale.o pg_lsn.o \
+ pg_upgrade_support.o pgstatfuncs.o \
pseudotypes.o quote.o rangetypes.o rangetypes_gist.o \
rangetypes_selfuncs.o rangetypes_spgist.o rangetypes_typanalyze.o \
regexp.o regproc.o ri_triggers.o rowtypes.o ruleutils.o \
diff --git a/src/backend/utils/adt/partitionfuncs.c b/src/backend/utils/adt/partitionfuncs.c
new file mode 100644
index 0000000000..fc0a904967
--- /dev/null
+++ b/src/backend/utils/adt/partitionfuncs.c
@@ -0,0 +1,128 @@
+/*-------------------------------------------------------------------------
+ *
+ * partitionfuncs.c
+ * Functions for accessing partitioning data
+ *
+ * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/adt/partitionfuncs.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "catalog/partition.h"
+#include "catalog/pg_inherits.h"
+#include "catalog/pg_type.h"
+#include "funcapi.h"
+#include "utils/fmgrprotos.h"
+
+/*
+ * pg_partition_tree
+ *
+ * Produce a view with one row per member of a partition tree, beginning
+ * from the top-most parent given by the caller. This gives information
+ * about each partition, its immediate partitioned parent and its level in
+ * the hierarchy.
+ */
+Datum
+pg_partition_tree(PG_FUNCTION_ARGS)
+{
+#define PG_PARTITION_TREE_COLS 3
+ Oid rootrelid = PG_GETARG_OID(0);
+ FuncCallContext *funcctx;
+ ListCell **next;
+
+ /* stuff done only on the first call of the function */
+ if (SRF_IS_FIRSTCALL())
+ {
+ MemoryContext oldcxt;
+ TupleDesc tupdesc;
+ List *partitions;
+
+ /* create a function context for cross-call persistence */
+ funcctx = SRF_FIRSTCALL_INIT();
+
+ /* switch to memory context appropriate for multiple function calls */
+ oldcxt = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ partitions = find_all_inheritors(rootrelid, NoLock, NULL);
+
+ tupdesc = CreateTemplateTupleDesc(PG_PARTITION_TREE_COLS, false);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "relid",
+ OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "parentid",
+ OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "level",
+ INT4OID, -1, 0);
+
+ funcctx->tuple_desc = BlessTupleDesc(tupdesc);
+
+ /* allocate memory for user context */
+ next = (ListCell **) palloc(sizeof(ListCell *));
+ *next = list_head(partitions);
+ funcctx->user_fctx = (void *) next;
+
+ MemoryContextSwitchTo(oldcxt);
+ }
+
+ /* stuff done on every call of the function */
+ funcctx = SRF_PERCALL_SETUP();
+ next = (ListCell **) funcctx->user_fctx;
+
+ if (*next != NULL)
+ {
+ Datum values[PG_PARTITION_TREE_COLS];
+ bool nulls[PG_PARTITION_TREE_COLS];
+ HeapTuple tuple;
+ Oid relid = lfirst_oid(*next);
+ List *ancestors = get_partition_ancestors(lfirst_oid(*next));
+ Oid parentid = InvalidOid;
+ int level = 0;
+ Datum result;
+ ListCell *lc;
+
+ /*
+ * Form tuple with appropriate data.
+ */
+ MemSet(nulls, 0, sizeof(nulls));
+ MemSet(values, 0, sizeof(values));
+
+ /* relid */
+ values[0] = ObjectIdGetDatum(relid);
+
+ /* parentid */
+ if (ancestors != NIL)
+ parentid = linitial_oid(ancestors);
+ if (OidIsValid(parentid))
+ values[1] = ObjectIdGetDatum(parentid);
+ else
+ nulls[1] = true;
+
+ /* level */
+ if (relid != rootrelid)
+ {
+ foreach(lc, ancestors)
+ {
+ level++;
+ if (lfirst_oid(lc) == rootrelid)
+ break;
+ }
+ }
+ values[2] = Int32GetDatum(level);
+
+ *next = lnext(*next);
+
+ tuple = heap_form_tuple(funcctx->tuple_desc, values, nulls);
+ result = HeapTupleGetDatum(tuple);
+ SRF_RETURN_NEXT(funcctx, result);
+ }
+
+ /* done when there are no more elements left */
+ SRF_RETURN_DONE(funcctx);
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 8e4145f42b..5525ad427b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10206,4 +10206,13 @@
proisstrict => 'f', prorettype => 'bool', proargtypes => 'oid int4 int4 any',
proargmodes => '{i,i,i,v}', prosrc => 'satisfies_hash_partition' },
+# information about a partition tree
+{ oid => '3423', descr => 'view partition tree tables',
+ proname => 'pg_partition_tree', prorows => '1000', proretset => 't',
+ provolatile => 'v', prorettype => 'record', proargtypes => 'oid',
+ proallargtypes => '{oid,oid,oid,int4}',
+ proargmodes => '{i,o,o,o}',
+ proargnames => '{rootrelid,relid,parentrelid,level}',
+ prosrc => 'pg_partition_tree' }
+
]
diff --git a/src/test/regress/expected/partition_info.out b/src/test/regress/expected/partition_info.out
new file mode 100644
index 0000000000..fed50420b5
--- /dev/null
+++ b/src/test/regress/expected/partition_info.out
@@ -0,0 +1,52 @@
+--
+-- Tests for pg_partition_tree
+--
+CREATE TABLE ptif_test (a int, b int) PARTITION BY range (a);
+CREATE TABLE ptif_test0 PARTITION OF ptif_test
+ FOR VALUES FROM (minvalue) TO (0) PARTITION BY list (b);
+CREATE TABLE ptif_test01 PARTITION OF ptif_test0 FOR VALUES IN (1);
+CREATE TABLE ptif_test1 PARTITION OF ptif_test
+ FOR VALUES FROM (0) TO (100) PARTITION BY list (b);
+CREATE TABLE ptif_test11 PARTITION OF ptif_test1 FOR VALUES IN (1);
+CREATE TABLE ptif_test2 PARTITION OF ptif_test
+ FOR VALUES FROM (100) TO (maxvalue);
+INSERT INTO ptif_test SELECT i, 1 FROM generate_series(-500, 500) i;
+-- all tables in the tree with some size information
+SELECT relid::regclass, parentrelid::regclass, level,
+ pg_relation_size(relid) = 0 AS is_empty
+ FROM pg_partition_tree('ptif_test'::regclass);
+ relid | parentrelid | level | is_empty
+-------------+-------------+-------+----------
+ ptif_test | | 0 | t
+ ptif_test0 | ptif_test | 1 | t
+ ptif_test1 | ptif_test | 1 | t
+ ptif_test2 | ptif_test | 1 | f
+ ptif_test01 | ptif_test0 | 2 | f
+ ptif_test11 | ptif_test1 | 2 | f
+(6 rows)
+
+-- children of the main tree
+SELECT relid::regclass, parentrelid::regclass, level
+ FROM pg_partition_tree('ptif_test0'::regclass);
+ relid | parentrelid | level
+-------------+-------------+-------
+ ptif_test0 | ptif_test | 0
+ ptif_test01 | ptif_test0 | 1
+(2 rows)
+
+SELECT relid::regclass, parentrelid::regclass, level
+ FROM pg_partition_tree('ptif_test01'::regclass);
+ relid | parentrelid | level
+-------------+-------------+-------
+ ptif_test01 | ptif_test0 | 0
+(1 row)
+
+-- this results in NULL, as there are no level 1 partitions of a leaf partition
+SELECT sum(pg_relation_size(relid)) AS total_size
+ FROM pg_partition_tree('ptif_test01'::regclass) WHERE level = 1;
+ total_size
+------------
+
+(1 row)
+
+DROP TABLE ptif_test;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 16f979c8d9..6cb820bbc4 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -116,7 +116,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid c
# ----------
# Another group of parallel tests
# ----------
-test: identity partition_join partition_prune reloptions hash_part indexing partition_aggregate
+test: identity partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info
# event triggers cannot run concurrently with any test that runs DDL
test: event_trigger
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 42632be675..7e374c2daa 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -188,6 +188,7 @@ test: reloptions
test: hash_part
test: indexing
test: partition_aggregate
+test: partition_info
test: event_trigger
test: fast_default
test: stats
diff --git a/src/test/regress/sql/partition_info.sql b/src/test/regress/sql/partition_info.sql
new file mode 100644
index 0000000000..2ad75882b0
--- /dev/null
+++ b/src/test/regress/sql/partition_info.sql
@@ -0,0 +1,30 @@
+--
+-- Tests for pg_partition_tree
+--
+CREATE TABLE ptif_test (a int, b int) PARTITION BY range (a);
+CREATE TABLE ptif_test0 PARTITION OF ptif_test
+ FOR VALUES FROM (minvalue) TO (0) PARTITION BY list (b);
+CREATE TABLE ptif_test01 PARTITION OF ptif_test0 FOR VALUES IN (1);
+CREATE TABLE ptif_test1 PARTITION OF ptif_test
+ FOR VALUES FROM (0) TO (100) PARTITION BY list (b);
+CREATE TABLE ptif_test11 PARTITION OF ptif_test1 FOR VALUES IN (1);
+CREATE TABLE ptif_test2 PARTITION OF ptif_test
+ FOR VALUES FROM (100) TO (maxvalue);
+INSERT INTO ptif_test SELECT i, 1 FROM generate_series(-500, 500) i;
+
+-- all tables in the tree with some size information
+SELECT relid::regclass, parentrelid::regclass, level,
+ pg_relation_size(relid) = 0 AS is_empty
+ FROM pg_partition_tree('ptif_test'::regclass);
+
+-- children of the main tree
+SELECT relid::regclass, parentrelid::regclass, level
+ FROM pg_partition_tree('ptif_test0'::regclass);
+SELECT relid::regclass, parentrelid::regclass, level
+ FROM pg_partition_tree('ptif_test01'::regclass);
+
+-- this results in NULL, as there are no level 1 partitions of a leaf partition
+SELECT sum(pg_relation_size(relid)) AS total_size
+ FROM pg_partition_tree('ptif_test01'::regclass) WHERE level = 1;
+
+DROP TABLE ptif_test;
[application/pgp-signature] signature.asc (833B, ../../[email protected]/3-signature.asc)
download
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-03 12:12 Jesper Pedersen <[email protected]>
parent: Michael Paquier <[email protected]>
2 siblings, 1 reply; 58+ messages in thread
From: Jesper Pedersen @ 2018-10-03 12:12 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; Amit Langote <[email protected]>; +Cc: Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
Hi Michael,
On 10/2/18 11:37 PM, Michael Paquier wrote:
> isleaf is not particularly useful as it can just be fetched with a join
> on pg_class/relkind. So I think that we had better remove it.
>
Removing isleaf would require extra round trips to the server to get
that information. So, I think we should keep it.
Best regards,
Jesper
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-04 00:27 Michael Paquier <[email protected]>
parent: Jesper Pedersen <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Michael Paquier @ 2018-10-04 00:27 UTC (permalink / raw)
To: Jesper Pedersen <[email protected]>; +Cc: Amit Langote <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On Wed, Oct 03, 2018 at 08:12:59AM -0400, Jesper Pedersen wrote:
> Removing isleaf would require extra round trips to the server to get
> that information. So, I think we should keep it.
I don't really get your point about extra round trips with the server,
and getting the same level of information is as simple as a join between
the result set of pg_partition_tree() and pg_class (better to add schema
qualification and aliases to relations by the way):
=# SELECT relid::regclass,
parentrelid::regclass, level,
relkind != 'p' AS isleaf
FROM pg_partition_tree('ptif_test'::regclass), pg_class
WHERE oid = relid;
relid | parentrelid | level | isleaf
-------------+-------------+-------+--------
ptif_test | null | 0 | f
ptif_test0 | ptif_test | 1 | f
ptif_test1 | ptif_test | 1 | f
ptif_test2 | ptif_test | 1 | t
ptif_test01 | ptif_test0 | 2 | t
ptif_test11 | ptif_test1 | 2 | t
(6 rows)
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-04 07:49 Amit Langote <[email protected]>
parent: Michael Paquier <[email protected]>
2 siblings, 0 replies; 58+ messages in thread
From: Amit Langote @ 2018-10-04 07:49 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: Thomas Munro <[email protected]>; Jesper Pedersen <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On 2018/10/03 12:37, Michael Paquier wrote:
> On Mon, Oct 01, 2018 at 04:27:57PM +0900, Amit Langote wrote:
>> Yeah, maybe there is no reason to delay proceeding with
>> pg_partition_children which provides a useful functionality.
>
> So, I have been looking at your patch, and there are a couple of things
> which could be improved.
Thanks for reviewing and updating the patch.
> Putting the new function pg_partition_children() in partition.c is a
> bad idea I think. So instead I think that we should put that in a
> different location, say utils/adt/partitionfuncs.c.
Okay, sounds like a good idea.
> + TupleDescInitEntry(tupdesc, (AttrNumber) 1, "relid",
> + REGCLASSOID, -1, 0);
> + TupleDescInitEntry(tupdesc, (AttrNumber) 2, "parentid",
> + REGCLASSOID, -1, 0);
> REGCLASSOID is used mainly for casting, so instead let's use OIDOID like
> any other system function.
Check.
> + values[2] = psprintf("%d", level);
> + values[3] = psprintf("%c", relkind == RELKIND_PARTITIONED_TABLE ?
> + 'f' :
> + 't');
> Using Datum objects is more elegant in this context.
Agreed. I think I'd just copied the psprintf code from some other function.
> isleaf is not particularly useful as it can just be fetched with a join
> on pg_class/relkind. So I think that we had better remove it.
That's a bit imposing on the users to know about relkind, but maybe that's
okay.
> I have cleaned up a bit tests, removing duplicates and most of the
> things which touched the size of relations to have something more
> portable.
Thanks for that.
> We could have a flavor using a relation name in input with qualified
> names handled properly (see pg_get_viewdef_name for example), not sure
> if that's really mandatory so I left that out.
Having to always use the typecast (::regclass) may be a bit annoying, but
we can always add that version later.
> I have also added some
> comments here and there. The docs could be worded a bit better still.
>
> My result is the patch attached. What do you think?
I looked at the updated patch and couldn't resist making some changes,
which see in the attached diff file. Also attached is the updated patch.
Thanks,
Amit
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index d41c09b68b..6dfa3dc977 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -20209,12 +20209,13 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
<entry><literal><function>pg_partition_tree(<type>oid</type>)</function></literal></entry>
<entry><type>setof record</type></entry>
<entry>
- List information about a partition tree for the given partitioned
- table, consisting of one row for each partition in a tree. The
- information available is the OID of the partition, the OID of its
- immediate partitioned table, and its level in the hierarchy,
- beginning at <literal>0</literal> for the top-most parent, and
- incremented by <literal>1</literal> for each level up.
+ List information about table in a partition tree for a given
+ partitioned table, which consists of one row for each partition and
+ table itself. Information provided includes the OID of the partition,
+ the OID of its immediate parent, and its level in the hierarchy.
+ The value of level begins at <literal>0</literal> for the input table
+ in its role as the root of the partition tree, <literal>1</literal> for
+ its partitions, <literal>2</literal> for their partitions, and so on.
</entry>
</row>
</tbody>
diff --git a/src/backend/utils/adt/partitionfuncs.c b/src/backend/utils/adt/partitionfuncs.c
index fc0a904967..41c57cac2d 100644
--- a/src/backend/utils/adt/partitionfuncs.c
+++ b/src/backend/utils/adt/partitionfuncs.c
@@ -1,7 +1,7 @@
/*-------------------------------------------------------------------------
*
* partitionfuncs.c
- * Functions for accessing partitioning data
+ * Functions for accessing partitioning related metadata
*
* Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
diff --git a/src/test/regress/sql/partition_info.sql b/src/test/regress/sql/partition_info.sql
index 2ad75882b0..d9d96a284c 100644
--- a/src/test/regress/sql/partition_info.sql
+++ b/src/test/regress/sql/partition_info.sql
@@ -17,11 +17,24 @@ SELECT relid::regclass, parentrelid::regclass, level,
pg_relation_size(relid) = 0 AS is_empty
FROM pg_partition_tree('ptif_test'::regclass);
--- children of the main tree
-SELECT relid::regclass, parentrelid::regclass, level
- FROM pg_partition_tree('ptif_test0'::regclass);
-SELECT relid::regclass, parentrelid::regclass, level
- FROM pg_partition_tree('ptif_test01'::regclass);
+-- check that it works correctly even if it's passed other tables in the tree
+
+-- passing an intermediate level partitioned table
+SELECT relid::regclass, parentrelid::regclass, level, relkind <> 'p' AS isleaf
+ FROM pg_partition_tree('ptif_test0'::regclass) p
+ JOIN pg_class c ON (p.relid = c.oid);
+
+-- passing a leaf partition
+SELECT relid::regclass, parentrelid::regclass, level, relkind <> 'p' AS isleaf
+ FROM pg_partition_tree('ptif_test01'::regclass) p
+ JOIN pg_class c ON (p.relid = c.oid);
+
+-- check that passing a table that's not part of any partition tree works
+-- the same as passing a leaf partition
+create table ptif_normal_table(a int);
+SELECT relid::regclass, parentrelid::regclass, level, relkind <> 'p' AS isleaf
+ FROM pg_partition_tree('ptif_normal_table'::regclass) p
+ JOIN pg_class c ON (p.relid = c.oid);
-- this results in NULL, as there are no level 1 partitions of a leaf partition
SELECT sum(pg_relation_size(relid)) AS total_size
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 9a7f683658..6dfa3dc977 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -20197,6 +20197,47 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
The function returns the number of new collation objects it created.
</para>
+ <table id="functions-info-partition">
+ <title>Partitioning Information Functions</title>
+ <tgroup cols="3">
+ <thead>
+ <row><entry>Name</entry> <entry>Return Type</entry> <entry>Description</entry></row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry><literal><function>pg_partition_tree(<type>oid</type>)</function></literal></entry>
+ <entry><type>setof record</type></entry>
+ <entry>
+ List information about table in a partition tree for a given
+ partitioned table, which consists of one row for each partition and
+ table itself. Information provided includes the OID of the partition,
+ the OID of its immediate parent, and its level in the hierarchy.
+ The value of level begins at <literal>0</literal> for the input table
+ in its role as the root of the partition tree, <literal>1</literal> for
+ its partitions, <literal>2</literal> for their partitions, and so on.
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ <para>
+ To check the total size of the data contained in
+ <structname>measurement</structname> table described in
+ <xref linkend="ddl-partitioning-declarative-example"/>, one could use the
+ following query:
+ </para>
+
+<programlisting>
+=# SELECT pg_size_pretty(sum(pg_relation_size(relid))) AS total_size
+ FROM pg_partition_tree('measurement');
+ total_size
+------------
+ 24 kB
+(1 row)
+</programlisting>
+
</sect2>
<sect2 id="functions-admin-index">
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 4b35dbb8bb..132ec7620c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -20,8 +20,8 @@ OBJS = acl.o amutils.o arrayfuncs.o array_expanded.o array_selfuncs.o \
jsonfuncs.o like.o lockfuncs.o mac.o mac8.o misc.o nabstime.o name.o \
network.o network_gist.o network_selfuncs.o network_spgist.o \
numeric.o numutils.o oid.o oracle_compat.o \
- orderedsetaggs.o pg_locale.o pg_lsn.o pg_upgrade_support.o \
- pgstatfuncs.o \
+ orderedsetaggs.o partitionfuncs.o pg_locale.o pg_lsn.o \
+ pg_upgrade_support.o pgstatfuncs.o \
pseudotypes.o quote.o rangetypes.o rangetypes_gist.o \
rangetypes_selfuncs.o rangetypes_spgist.o rangetypes_typanalyze.o \
regexp.o regproc.o ri_triggers.o rowtypes.o ruleutils.o \
diff --git a/src/backend/utils/adt/partitionfuncs.c b/src/backend/utils/adt/partitionfuncs.c
new file mode 100644
index 0000000000..41c57cac2d
--- /dev/null
+++ b/src/backend/utils/adt/partitionfuncs.c
@@ -0,0 +1,128 @@
+/*-------------------------------------------------------------------------
+ *
+ * partitionfuncs.c
+ * Functions for accessing partitioning related metadata
+ *
+ * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/adt/partitionfuncs.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "catalog/partition.h"
+#include "catalog/pg_inherits.h"
+#include "catalog/pg_type.h"
+#include "funcapi.h"
+#include "utils/fmgrprotos.h"
+
+/*
+ * pg_partition_tree
+ *
+ * Produce a view with one row per member of a partition tree, beginning
+ * from the top-most parent given by the caller. This gives information
+ * about each partition, its immediate partitioned parent and its level in
+ * the hierarchy.
+ */
+Datum
+pg_partition_tree(PG_FUNCTION_ARGS)
+{
+#define PG_PARTITION_TREE_COLS 3
+ Oid rootrelid = PG_GETARG_OID(0);
+ FuncCallContext *funcctx;
+ ListCell **next;
+
+ /* stuff done only on the first call of the function */
+ if (SRF_IS_FIRSTCALL())
+ {
+ MemoryContext oldcxt;
+ TupleDesc tupdesc;
+ List *partitions;
+
+ /* create a function context for cross-call persistence */
+ funcctx = SRF_FIRSTCALL_INIT();
+
+ /* switch to memory context appropriate for multiple function calls */
+ oldcxt = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ partitions = find_all_inheritors(rootrelid, NoLock, NULL);
+
+ tupdesc = CreateTemplateTupleDesc(PG_PARTITION_TREE_COLS, false);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "relid",
+ OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "parentid",
+ OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "level",
+ INT4OID, -1, 0);
+
+ funcctx->tuple_desc = BlessTupleDesc(tupdesc);
+
+ /* allocate memory for user context */
+ next = (ListCell **) palloc(sizeof(ListCell *));
+ *next = list_head(partitions);
+ funcctx->user_fctx = (void *) next;
+
+ MemoryContextSwitchTo(oldcxt);
+ }
+
+ /* stuff done on every call of the function */
+ funcctx = SRF_PERCALL_SETUP();
+ next = (ListCell **) funcctx->user_fctx;
+
+ if (*next != NULL)
+ {
+ Datum values[PG_PARTITION_TREE_COLS];
+ bool nulls[PG_PARTITION_TREE_COLS];
+ HeapTuple tuple;
+ Oid relid = lfirst_oid(*next);
+ List *ancestors = get_partition_ancestors(lfirst_oid(*next));
+ Oid parentid = InvalidOid;
+ int level = 0;
+ Datum result;
+ ListCell *lc;
+
+ /*
+ * Form tuple with appropriate data.
+ */
+ MemSet(nulls, 0, sizeof(nulls));
+ MemSet(values, 0, sizeof(values));
+
+ /* relid */
+ values[0] = ObjectIdGetDatum(relid);
+
+ /* parentid */
+ if (ancestors != NIL)
+ parentid = linitial_oid(ancestors);
+ if (OidIsValid(parentid))
+ values[1] = ObjectIdGetDatum(parentid);
+ else
+ nulls[1] = true;
+
+ /* level */
+ if (relid != rootrelid)
+ {
+ foreach(lc, ancestors)
+ {
+ level++;
+ if (lfirst_oid(lc) == rootrelid)
+ break;
+ }
+ }
+ values[2] = Int32GetDatum(level);
+
+ *next = lnext(*next);
+
+ tuple = heap_form_tuple(funcctx->tuple_desc, values, nulls);
+ result = HeapTupleGetDatum(tuple);
+ SRF_RETURN_NEXT(funcctx, result);
+ }
+
+ /* done when there are no more elements left */
+ SRF_RETURN_DONE(funcctx);
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 8e4145f42b..5525ad427b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10206,4 +10206,13 @@
proisstrict => 'f', prorettype => 'bool', proargtypes => 'oid int4 int4 any',
proargmodes => '{i,i,i,v}', prosrc => 'satisfies_hash_partition' },
+# information about a partition tree
+{ oid => '3423', descr => 'view partition tree tables',
+ proname => 'pg_partition_tree', prorows => '1000', proretset => 't',
+ provolatile => 'v', prorettype => 'record', proargtypes => 'oid',
+ proallargtypes => '{oid,oid,oid,int4}',
+ proargmodes => '{i,o,o,o}',
+ proargnames => '{rootrelid,relid,parentrelid,level}',
+ prosrc => 'pg_partition_tree' }
+
]
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 16f979c8d9..6cb820bbc4 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -116,7 +116,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid c
# ----------
# Another group of parallel tests
# ----------
-test: identity partition_join partition_prune reloptions hash_part indexing partition_aggregate
+test: identity partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info
# event triggers cannot run concurrently with any test that runs DDL
test: event_trigger
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 42632be675..7e374c2daa 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -188,6 +188,7 @@ test: reloptions
test: hash_part
test: indexing
test: partition_aggregate
+test: partition_info
test: event_trigger
test: fast_default
test: stats
diff --git a/src/test/regress/sql/partition_info.sql b/src/test/regress/sql/partition_info.sql
new file mode 100644
index 0000000000..d9d96a284c
--- /dev/null
+++ b/src/test/regress/sql/partition_info.sql
@@ -0,0 +1,43 @@
+--
+-- Tests for pg_partition_tree
+--
+CREATE TABLE ptif_test (a int, b int) PARTITION BY range (a);
+CREATE TABLE ptif_test0 PARTITION OF ptif_test
+ FOR VALUES FROM (minvalue) TO (0) PARTITION BY list (b);
+CREATE TABLE ptif_test01 PARTITION OF ptif_test0 FOR VALUES IN (1);
+CREATE TABLE ptif_test1 PARTITION OF ptif_test
+ FOR VALUES FROM (0) TO (100) PARTITION BY list (b);
+CREATE TABLE ptif_test11 PARTITION OF ptif_test1 FOR VALUES IN (1);
+CREATE TABLE ptif_test2 PARTITION OF ptif_test
+ FOR VALUES FROM (100) TO (maxvalue);
+INSERT INTO ptif_test SELECT i, 1 FROM generate_series(-500, 500) i;
+
+-- all tables in the tree with some size information
+SELECT relid::regclass, parentrelid::regclass, level,
+ pg_relation_size(relid) = 0 AS is_empty
+ FROM pg_partition_tree('ptif_test'::regclass);
+
+-- check that it works correctly even if it's passed other tables in the tree
+
+-- passing an intermediate level partitioned table
+SELECT relid::regclass, parentrelid::regclass, level, relkind <> 'p' AS isleaf
+ FROM pg_partition_tree('ptif_test0'::regclass) p
+ JOIN pg_class c ON (p.relid = c.oid);
+
+-- passing a leaf partition
+SELECT relid::regclass, parentrelid::regclass, level, relkind <> 'p' AS isleaf
+ FROM pg_partition_tree('ptif_test01'::regclass) p
+ JOIN pg_class c ON (p.relid = c.oid);
+
+-- check that passing a table that's not part of any partition tree works
+-- the same as passing a leaf partition
+create table ptif_normal_table(a int);
+SELECT relid::regclass, parentrelid::regclass, level, relkind <> 'p' AS isleaf
+ FROM pg_partition_tree('ptif_normal_table'::regclass) p
+ JOIN pg_class c ON (p.relid = c.oid);
+
+-- this results in NULL, as there are no level 1 partitions of a leaf partition
+SELECT sum(pg_relation_size(relid)) AS total_size
+ FROM pg_partition_tree('ptif_test01'::regclass) WHERE level = 1;
+
+DROP TABLE ptif_test;
Attachments:
[text/plain] michael_v12_to_v13.diff (3.6K, ../../[email protected]/2-michael_v12_to_v13.diff)
download | inline diff:
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index d41c09b68b..6dfa3dc977 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -20209,12 +20209,13 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
<entry><literal><function>pg_partition_tree(<type>oid</type>)</function></literal></entry>
<entry><type>setof record</type></entry>
<entry>
- List information about a partition tree for the given partitioned
- table, consisting of one row for each partition in a tree. The
- information available is the OID of the partition, the OID of its
- immediate partitioned table, and its level in the hierarchy,
- beginning at <literal>0</literal> for the top-most parent, and
- incremented by <literal>1</literal> for each level up.
+ List information about table in a partition tree for a given
+ partitioned table, which consists of one row for each partition and
+ table itself. Information provided includes the OID of the partition,
+ the OID of its immediate parent, and its level in the hierarchy.
+ The value of level begins at <literal>0</literal> for the input table
+ in its role as the root of the partition tree, <literal>1</literal> for
+ its partitions, <literal>2</literal> for their partitions, and so on.
</entry>
</row>
</tbody>
diff --git a/src/backend/utils/adt/partitionfuncs.c b/src/backend/utils/adt/partitionfuncs.c
index fc0a904967..41c57cac2d 100644
--- a/src/backend/utils/adt/partitionfuncs.c
+++ b/src/backend/utils/adt/partitionfuncs.c
@@ -1,7 +1,7 @@
/*-------------------------------------------------------------------------
*
* partitionfuncs.c
- * Functions for accessing partitioning data
+ * Functions for accessing partitioning related metadata
*
* Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
* Portions Copyright (c) 1994, Regents of the University of California
diff --git a/src/test/regress/sql/partition_info.sql b/src/test/regress/sql/partition_info.sql
index 2ad75882b0..d9d96a284c 100644
--- a/src/test/regress/sql/partition_info.sql
+++ b/src/test/regress/sql/partition_info.sql
@@ -17,11 +17,24 @@ SELECT relid::regclass, parentrelid::regclass, level,
pg_relation_size(relid) = 0 AS is_empty
FROM pg_partition_tree('ptif_test'::regclass);
--- children of the main tree
-SELECT relid::regclass, parentrelid::regclass, level
- FROM pg_partition_tree('ptif_test0'::regclass);
-SELECT relid::regclass, parentrelid::regclass, level
- FROM pg_partition_tree('ptif_test01'::regclass);
+-- check that it works correctly even if it's passed other tables in the tree
+
+-- passing an intermediate level partitioned table
+SELECT relid::regclass, parentrelid::regclass, level, relkind <> 'p' AS isleaf
+ FROM pg_partition_tree('ptif_test0'::regclass) p
+ JOIN pg_class c ON (p.relid = c.oid);
+
+-- passing a leaf partition
+SELECT relid::regclass, parentrelid::regclass, level, relkind <> 'p' AS isleaf
+ FROM pg_partition_tree('ptif_test01'::regclass) p
+ JOIN pg_class c ON (p.relid = c.oid);
+
+-- check that passing a table that's not part of any partition tree works
+-- the same as passing a leaf partition
+create table ptif_normal_table(a int);
+SELECT relid::regclass, parentrelid::regclass, level, relkind <> 'p' AS isleaf
+ FROM pg_partition_tree('ptif_normal_table'::regclass) p
+ JOIN pg_class c ON (p.relid = c.oid);
-- this results in NULL, as there are no level 1 partitions of a leaf partition
SELECT sum(pg_relation_size(relid)) AS total_size
[text/plain] partition-tree-func-v13.patch (10.1K, ../../[email protected]/3-partition-tree-func-v13.patch)
download | inline diff:
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 9a7f683658..6dfa3dc977 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -20197,6 +20197,47 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
The function returns the number of new collation objects it created.
</para>
+ <table id="functions-info-partition">
+ <title>Partitioning Information Functions</title>
+ <tgroup cols="3">
+ <thead>
+ <row><entry>Name</entry> <entry>Return Type</entry> <entry>Description</entry></row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry><literal><function>pg_partition_tree(<type>oid</type>)</function></literal></entry>
+ <entry><type>setof record</type></entry>
+ <entry>
+ List information about table in a partition tree for a given
+ partitioned table, which consists of one row for each partition and
+ table itself. Information provided includes the OID of the partition,
+ the OID of its immediate parent, and its level in the hierarchy.
+ The value of level begins at <literal>0</literal> for the input table
+ in its role as the root of the partition tree, <literal>1</literal> for
+ its partitions, <literal>2</literal> for their partitions, and so on.
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ <para>
+ To check the total size of the data contained in
+ <structname>measurement</structname> table described in
+ <xref linkend="ddl-partitioning-declarative-example"/>, one could use the
+ following query:
+ </para>
+
+<programlisting>
+=# SELECT pg_size_pretty(sum(pg_relation_size(relid))) AS total_size
+ FROM pg_partition_tree('measurement');
+ total_size
+------------
+ 24 kB
+(1 row)
+</programlisting>
+
</sect2>
<sect2 id="functions-admin-index">
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 4b35dbb8bb..132ec7620c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -20,8 +20,8 @@ OBJS = acl.o amutils.o arrayfuncs.o array_expanded.o array_selfuncs.o \
jsonfuncs.o like.o lockfuncs.o mac.o mac8.o misc.o nabstime.o name.o \
network.o network_gist.o network_selfuncs.o network_spgist.o \
numeric.o numutils.o oid.o oracle_compat.o \
- orderedsetaggs.o pg_locale.o pg_lsn.o pg_upgrade_support.o \
- pgstatfuncs.o \
+ orderedsetaggs.o partitionfuncs.o pg_locale.o pg_lsn.o \
+ pg_upgrade_support.o pgstatfuncs.o \
pseudotypes.o quote.o rangetypes.o rangetypes_gist.o \
rangetypes_selfuncs.o rangetypes_spgist.o rangetypes_typanalyze.o \
regexp.o regproc.o ri_triggers.o rowtypes.o ruleutils.o \
diff --git a/src/backend/utils/adt/partitionfuncs.c b/src/backend/utils/adt/partitionfuncs.c
new file mode 100644
index 0000000000..41c57cac2d
--- /dev/null
+++ b/src/backend/utils/adt/partitionfuncs.c
@@ -0,0 +1,128 @@
+/*-------------------------------------------------------------------------
+ *
+ * partitionfuncs.c
+ * Functions for accessing partitioning related metadata
+ *
+ * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/adt/partitionfuncs.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "catalog/partition.h"
+#include "catalog/pg_inherits.h"
+#include "catalog/pg_type.h"
+#include "funcapi.h"
+#include "utils/fmgrprotos.h"
+
+/*
+ * pg_partition_tree
+ *
+ * Produce a view with one row per member of a partition tree, beginning
+ * from the top-most parent given by the caller. This gives information
+ * about each partition, its immediate partitioned parent and its level in
+ * the hierarchy.
+ */
+Datum
+pg_partition_tree(PG_FUNCTION_ARGS)
+{
+#define PG_PARTITION_TREE_COLS 3
+ Oid rootrelid = PG_GETARG_OID(0);
+ FuncCallContext *funcctx;
+ ListCell **next;
+
+ /* stuff done only on the first call of the function */
+ if (SRF_IS_FIRSTCALL())
+ {
+ MemoryContext oldcxt;
+ TupleDesc tupdesc;
+ List *partitions;
+
+ /* create a function context for cross-call persistence */
+ funcctx = SRF_FIRSTCALL_INIT();
+
+ /* switch to memory context appropriate for multiple function calls */
+ oldcxt = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ partitions = find_all_inheritors(rootrelid, NoLock, NULL);
+
+ tupdesc = CreateTemplateTupleDesc(PG_PARTITION_TREE_COLS, false);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "relid",
+ OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "parentid",
+ OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "level",
+ INT4OID, -1, 0);
+
+ funcctx->tuple_desc = BlessTupleDesc(tupdesc);
+
+ /* allocate memory for user context */
+ next = (ListCell **) palloc(sizeof(ListCell *));
+ *next = list_head(partitions);
+ funcctx->user_fctx = (void *) next;
+
+ MemoryContextSwitchTo(oldcxt);
+ }
+
+ /* stuff done on every call of the function */
+ funcctx = SRF_PERCALL_SETUP();
+ next = (ListCell **) funcctx->user_fctx;
+
+ if (*next != NULL)
+ {
+ Datum values[PG_PARTITION_TREE_COLS];
+ bool nulls[PG_PARTITION_TREE_COLS];
+ HeapTuple tuple;
+ Oid relid = lfirst_oid(*next);
+ List *ancestors = get_partition_ancestors(lfirst_oid(*next));
+ Oid parentid = InvalidOid;
+ int level = 0;
+ Datum result;
+ ListCell *lc;
+
+ /*
+ * Form tuple with appropriate data.
+ */
+ MemSet(nulls, 0, sizeof(nulls));
+ MemSet(values, 0, sizeof(values));
+
+ /* relid */
+ values[0] = ObjectIdGetDatum(relid);
+
+ /* parentid */
+ if (ancestors != NIL)
+ parentid = linitial_oid(ancestors);
+ if (OidIsValid(parentid))
+ values[1] = ObjectIdGetDatum(parentid);
+ else
+ nulls[1] = true;
+
+ /* level */
+ if (relid != rootrelid)
+ {
+ foreach(lc, ancestors)
+ {
+ level++;
+ if (lfirst_oid(lc) == rootrelid)
+ break;
+ }
+ }
+ values[2] = Int32GetDatum(level);
+
+ *next = lnext(*next);
+
+ tuple = heap_form_tuple(funcctx->tuple_desc, values, nulls);
+ result = HeapTupleGetDatum(tuple);
+ SRF_RETURN_NEXT(funcctx, result);
+ }
+
+ /* done when there are no more elements left */
+ SRF_RETURN_DONE(funcctx);
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 8e4145f42b..5525ad427b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10206,4 +10206,13 @@
proisstrict => 'f', prorettype => 'bool', proargtypes => 'oid int4 int4 any',
proargmodes => '{i,i,i,v}', prosrc => 'satisfies_hash_partition' },
+# information about a partition tree
+{ oid => '3423', descr => 'view partition tree tables',
+ proname => 'pg_partition_tree', prorows => '1000', proretset => 't',
+ provolatile => 'v', prorettype => 'record', proargtypes => 'oid',
+ proallargtypes => '{oid,oid,oid,int4}',
+ proargmodes => '{i,o,o,o}',
+ proargnames => '{rootrelid,relid,parentrelid,level}',
+ prosrc => 'pg_partition_tree' }
+
]
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 16f979c8d9..6cb820bbc4 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -116,7 +116,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid c
# ----------
# Another group of parallel tests
# ----------
-test: identity partition_join partition_prune reloptions hash_part indexing partition_aggregate
+test: identity partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info
# event triggers cannot run concurrently with any test that runs DDL
test: event_trigger
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 42632be675..7e374c2daa 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -188,6 +188,7 @@ test: reloptions
test: hash_part
test: indexing
test: partition_aggregate
+test: partition_info
test: event_trigger
test: fast_default
test: stats
diff --git a/src/test/regress/sql/partition_info.sql b/src/test/regress/sql/partition_info.sql
new file mode 100644
index 0000000000..d9d96a284c
--- /dev/null
+++ b/src/test/regress/sql/partition_info.sql
@@ -0,0 +1,43 @@
+--
+-- Tests for pg_partition_tree
+--
+CREATE TABLE ptif_test (a int, b int) PARTITION BY range (a);
+CREATE TABLE ptif_test0 PARTITION OF ptif_test
+ FOR VALUES FROM (minvalue) TO (0) PARTITION BY list (b);
+CREATE TABLE ptif_test01 PARTITION OF ptif_test0 FOR VALUES IN (1);
+CREATE TABLE ptif_test1 PARTITION OF ptif_test
+ FOR VALUES FROM (0) TO (100) PARTITION BY list (b);
+CREATE TABLE ptif_test11 PARTITION OF ptif_test1 FOR VALUES IN (1);
+CREATE TABLE ptif_test2 PARTITION OF ptif_test
+ FOR VALUES FROM (100) TO (maxvalue);
+INSERT INTO ptif_test SELECT i, 1 FROM generate_series(-500, 500) i;
+
+-- all tables in the tree with some size information
+SELECT relid::regclass, parentrelid::regclass, level,
+ pg_relation_size(relid) = 0 AS is_empty
+ FROM pg_partition_tree('ptif_test'::regclass);
+
+-- check that it works correctly even if it's passed other tables in the tree
+
+-- passing an intermediate level partitioned table
+SELECT relid::regclass, parentrelid::regclass, level, relkind <> 'p' AS isleaf
+ FROM pg_partition_tree('ptif_test0'::regclass) p
+ JOIN pg_class c ON (p.relid = c.oid);
+
+-- passing a leaf partition
+SELECT relid::regclass, parentrelid::regclass, level, relkind <> 'p' AS isleaf
+ FROM pg_partition_tree('ptif_test01'::regclass) p
+ JOIN pg_class c ON (p.relid = c.oid);
+
+-- check that passing a table that's not part of any partition tree works
+-- the same as passing a leaf partition
+create table ptif_normal_table(a int);
+SELECT relid::regclass, parentrelid::regclass, level, relkind <> 'p' AS isleaf
+ FROM pg_partition_tree('ptif_normal_table'::regclass) p
+ JOIN pg_class c ON (p.relid = c.oid);
+
+-- this results in NULL, as there are no level 1 partitions of a leaf partition
+SELECT sum(pg_relation_size(relid)) AS total_size
+ FROM pg_partition_tree('ptif_test01'::regclass) WHERE level = 1;
+
+DROP TABLE ptif_test;
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-04 07:53 Amit Langote <[email protected]>
parent: Michael Paquier <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Amit Langote @ 2018-10-04 07:53 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; Jesper Pedersen <[email protected]>; +Cc: Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On 2018/10/04 9:27, Michael Paquier wrote:
> On Wed, Oct 03, 2018 at 08:12:59AM -0400, Jesper Pedersen wrote:
>> Removing isleaf would require extra round trips to the server to get
>> that information. So, I think we should keep it.
>
> I don't really get your point about extra round trips with the server,
> and getting the same level of information is as simple as a join between
> the result set of pg_partition_tree() and pg_class (better to add schema
> qualification and aliases to relations by the way):
> =# SELECT relid::regclass,
> parentrelid::regclass, level,
> relkind != 'p' AS isleaf
> FROM pg_partition_tree('ptif_test'::regclass), pg_class
> WHERE oid = relid;
> relid | parentrelid | level | isleaf
> -------------+-------------+-------+--------
> ptif_test | null | 0 | f
> ptif_test0 | ptif_test | 1 | f
> ptif_test1 | ptif_test | 1 | f
> ptif_test2 | ptif_test | 1 | t
> ptif_test01 | ptif_test0 | 2 | t
> ptif_test11 | ptif_test1 | 2 | t
> (6 rows)
As mentioned in my other reply, that might be considered as asking the
user to know inner details like relkind. Also, if a database has many
partitioned tables with lots of partitions, the pg_class join might get
expensive. OTOH, computing and returning it with other fields of
pg_partition_tree is essentially free.
Thanks,
Amit
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-04 08:18 Michael Paquier <[email protected]>
parent: Amit Langote <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Michael Paquier @ 2018-10-04 08:18 UTC (permalink / raw)
To: Amit Langote <[email protected]>; +Cc: Jesper Pedersen <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On Thu, Oct 04, 2018 at 04:53:02PM +0900, Amit Langote wrote:
> As mentioned in my other reply, that might be considered as asking the
> user to know inner details like relkind. Also, if a database has many
> partitioned tables with lots of partitions, the pg_class join might get
> expensive. OTOH, computing and returning it with other fields of
> pg_partition_tree is essentially free.
So it seems that I am clearly outvoted here ;)
Okay, let's do as you folks propose.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-05 05:56 Michael Paquier <[email protected]>
parent: Michael Paquier <[email protected]>
0 siblings, 2 replies; 58+ messages in thread
From: Michael Paquier @ 2018-10-05 05:56 UTC (permalink / raw)
To: Amit Langote <[email protected]>; +Cc: Jesper Pedersen <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On Thu, Oct 04, 2018 at 05:18:07PM +0900, Michael Paquier wrote:
> So it seems that I am clearly outvoted here ;)
>
> Okay, let's do as you folks propose.
And attached is a newer version with this isleaf stuff and the previous
feedback from Amit integrated, as long as I recall about it. The
version is indented, and tutti-quanti. Does that look fine to everybody
here?
The CF bot should normally pick up this patch, the previous garbage seen
in one of the tests seems to come from the previous implementation which
used strings...
--
Michael
Attachments:
[text/x-diff] 0001-Add-pg_partition_tree-to-display-information-about-p.patch (15.2K, ../../[email protected]/2-0001-Add-pg_partition_tree-to-display-information-about-p.patch)
download | inline diff:
From 11bf47ee3137b6a15699bfda50b0904f2e10a415 Mon Sep 17 00:00:00 2001
From: Michael Paquier <[email protected]>
Date: Fri, 5 Oct 2018 14:41:17 +0900
Subject: [PATCH] Add pg_partition_tree to display information about partitions
This new function is useful to display a full tree of partitions with a
partitioned table given in output, and avoids the need of any complex
WITH RECURSIVE when looking at partition trees which are multi-level
deep.
It returns a set of records, one for each partition, containing the
partition OID, its immediate parent OID, if the relation is a leaf in
the tree and its level in the partition tree with given table considered
as root, beginning at zero for the root, and incrementing by one each
time the scan goes one level down.
Author: Amit Langote
Reviewed-by: Jesper Pedersen, Michael Paquier
Discussion: https://postgr.es/m/[email protected]
---
doc/src/sgml/func.sgml | 42 ++++++
src/backend/utils/adt/Makefile | 4 +-
src/backend/utils/adt/partitionfuncs.c | 137 +++++++++++++++++++
src/include/catalog/catversion.h | 2 +-
src/include/catalog/pg_proc.dat | 9 ++
src/test/regress/expected/partition_info.out | 67 +++++++++
src/test/regress/parallel_schedule | 2 +-
src/test/regress/serial_schedule | 1 +
src/test/regress/sql/partition_info.sql | 42 ++++++
9 files changed, 302 insertions(+), 4 deletions(-)
create mode 100644 src/backend/utils/adt/partitionfuncs.c
create mode 100644 src/test/regress/expected/partition_info.out
create mode 100644 src/test/regress/sql/partition_info.sql
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index f984d069e1..be315aaabb 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -20197,6 +20197,48 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
The function returns the number of new collation objects it created.
</para>
+ <table id="functions-info-partition">
+ <title>Partitioning Information Functions</title>
+ <tgroup cols="3">
+ <thead>
+ <row><entry>Name</entry> <entry>Return Type</entry> <entry>Description</entry></row>
+ </thead>
+
+ <tbody>
+ <row>
+ <entry><literal><function>pg_partition_tree(<type>oid</type>)</function></literal></entry>
+ <entry><type>setof record</type></entry>
+ <entry>
+ List information about table in a partition tree for a given
+ partitioned table, which consists of one row for each partition and
+ table itself. Information provided includes the OID of the partition,
+ the OID of its immediate parent, if the partition is a leaf and its
+ level in the hierarchy. The value of level begins at
+ <literal>0</literal> for the input table in its role as the root of
+ the partition tree, <literal>1</literal> for its partitions,
+ <literal>2</literal> for their partitions, and so on.
+ </entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ <para>
+ To check the total size of the data contained in
+ <structname>measurement</structname> table described in
+ <xref linkend="ddl-partitioning-declarative-example"/>, one could use the
+ following query:
+ </para>
+
+<programlisting>
+=# SELECT pg_size_pretty(sum(pg_relation_size(relid))) AS total_size
+ FROM pg_partition_tree('measurement'::regclass);
+ total_size
+------------
+ 24 kB
+(1 row)
+</programlisting>
+
</sect2>
<sect2 id="functions-admin-index">
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 4b35dbb8bb..132ec7620c 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -20,8 +20,8 @@ OBJS = acl.o amutils.o arrayfuncs.o array_expanded.o array_selfuncs.o \
jsonfuncs.o like.o lockfuncs.o mac.o mac8.o misc.o nabstime.o name.o \
network.o network_gist.o network_selfuncs.o network_spgist.o \
numeric.o numutils.o oid.o oracle_compat.o \
- orderedsetaggs.o pg_locale.o pg_lsn.o pg_upgrade_support.o \
- pgstatfuncs.o \
+ orderedsetaggs.o partitionfuncs.o pg_locale.o pg_lsn.o \
+ pg_upgrade_support.o pgstatfuncs.o \
pseudotypes.o quote.o rangetypes.o rangetypes_gist.o \
rangetypes_selfuncs.o rangetypes_spgist.o rangetypes_typanalyze.o \
regexp.o regproc.o ri_triggers.o rowtypes.o ruleutils.o \
diff --git a/src/backend/utils/adt/partitionfuncs.c b/src/backend/utils/adt/partitionfuncs.c
new file mode 100644
index 0000000000..c1743524c8
--- /dev/null
+++ b/src/backend/utils/adt/partitionfuncs.c
@@ -0,0 +1,137 @@
+/*-------------------------------------------------------------------------
+ *
+ * partitionfuncs.c
+ * Functions for accessing partition-related metadata
+ *
+ * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ * src/backend/utils/adt/partitionfuncs.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "access/htup_details.h"
+#include "catalog/partition.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_inherits.h"
+#include "catalog/pg_type.h"
+#include "funcapi.h"
+#include "utils/fmgrprotos.h"
+#include "utils/lsyscache.h"
+
+
+/*
+ * pg_partition_tree
+ *
+ * Produce a view with one row per member of a partition tree, beginning
+ * from the top-most parent given by the caller. This gives information
+ * about each partition, its immediate partitioned parent, if it is
+ * a leaf partition and its level in the hierarchy.
+ */
+Datum
+pg_partition_tree(PG_FUNCTION_ARGS)
+{
+#define PG_PARTITION_TREE_COLS 4
+ Oid rootrelid = PG_GETARG_OID(0);
+ FuncCallContext *funcctx;
+ ListCell **next;
+
+ /* stuff done only on the first call of the function */
+ if (SRF_IS_FIRSTCALL())
+ {
+ MemoryContext oldcxt;
+ TupleDesc tupdesc;
+ List *partitions;
+
+ /* create a function context for cross-call persistence */
+ funcctx = SRF_FIRSTCALL_INIT();
+
+ /* switch to memory context appropriate for multiple function calls */
+ oldcxt = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+ partitions = find_all_inheritors(rootrelid, NoLock, NULL);
+
+ tupdesc = CreateTemplateTupleDesc(PG_PARTITION_TREE_COLS, false);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 1, "relid",
+ OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 2, "parentid",
+ OIDOID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 3, "isleaf",
+ BOOLOID, -1, 0);
+ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "level",
+ INT4OID, -1, 0);
+
+ funcctx->tuple_desc = BlessTupleDesc(tupdesc);
+
+ /* allocate memory for user context */
+ next = (ListCell **) palloc(sizeof(ListCell *));
+ *next = list_head(partitions);
+ funcctx->user_fctx = (void *) next;
+
+ MemoryContextSwitchTo(oldcxt);
+ }
+
+ /* stuff done on every call of the function */
+ funcctx = SRF_PERCALL_SETUP();
+ next = (ListCell **) funcctx->user_fctx;
+
+ if (*next != NULL)
+ {
+ Datum values[PG_PARTITION_TREE_COLS];
+ bool nulls[PG_PARTITION_TREE_COLS];
+ HeapTuple tuple;
+ Oid relid = lfirst_oid(*next);
+ char relkind = get_rel_relkind(relid);
+ List *ancestors = get_partition_ancestors(lfirst_oid(*next));
+ Oid parentid = InvalidOid;
+ int level = 0;
+ Datum result;
+ ListCell *lc;
+
+ /*
+ * Form tuple with appropriate data.
+ */
+ MemSet(nulls, 0, sizeof(nulls));
+ MemSet(values, 0, sizeof(values));
+
+ /* relid */
+ values[0] = ObjectIdGetDatum(relid);
+
+ /* parentid */
+ if (ancestors != NIL)
+ parentid = linitial_oid(ancestors);
+ if (OidIsValid(parentid))
+ values[1] = ObjectIdGetDatum(parentid);
+ else
+ nulls[1] = true;
+
+ /* isleaf */
+ values[2] = BoolGetDatum(relkind != RELKIND_PARTITIONED_TABLE);
+
+ /* level */
+ if (relid != rootrelid)
+ {
+ foreach(lc, ancestors)
+ {
+ level++;
+ if (lfirst_oid(lc) == rootrelid)
+ break;
+ }
+ }
+ values[3] = Int32GetDatum(level);
+
+ *next = lnext(*next);
+
+ tuple = heap_form_tuple(funcctx->tuple_desc, values, nulls);
+ result = HeapTupleGetDatum(tuple);
+ SRF_RETURN_NEXT(funcctx, result);
+ }
+
+ /* done when there are no more elements left */
+ SRF_RETURN_DONE(funcctx);
+}
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index 89e2b60916..0d44a7c680 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -53,6 +53,6 @@
*/
/* yyyymmddN */
-#define CATALOG_VERSION_NO 201810051
+#define CATALOG_VERSION_NO 201810052
#endif
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 8579822bcd..a234a45b60 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -10216,4 +10216,13 @@
proisstrict => 'f', prorettype => 'bool', proargtypes => 'oid int4 int4 any',
proargmodes => '{i,i,i,v}', prosrc => 'satisfies_hash_partition' },
+# information about a partition tree
+{ oid => '3423', descr => 'view partition tree tables',
+ proname => 'pg_partition_tree', prorows => '1000', proretset => 't',
+ provolatile => 'v', prorettype => 'record', proargtypes => 'oid',
+ proallargtypes => '{oid,oid,oid,bool,int4}',
+ proargmodes => '{i,o,o,o,o}',
+ proargnames => '{rootrelid,relid,parentrelid,isleaf,level}',
+ prosrc => 'pg_partition_tree' }
+
]
diff --git a/src/test/regress/expected/partition_info.out b/src/test/regress/expected/partition_info.out
new file mode 100644
index 0000000000..6771a1ddb1
--- /dev/null
+++ b/src/test/regress/expected/partition_info.out
@@ -0,0 +1,67 @@
+--
+-- Tests for pg_partition_tree
+--
+CREATE TABLE ptif_test (a int, b int) PARTITION BY range (a);
+CREATE TABLE ptif_test0 PARTITION OF ptif_test
+ FOR VALUES FROM (minvalue) TO (0) PARTITION BY list (b);
+CREATE TABLE ptif_test01 PARTITION OF ptif_test0 FOR VALUES IN (1);
+CREATE TABLE ptif_test1 PARTITION OF ptif_test
+ FOR VALUES FROM (0) TO (100) PARTITION BY list (b);
+CREATE TABLE ptif_test11 PARTITION OF ptif_test1 FOR VALUES IN (1);
+CREATE TABLE ptif_test2 PARTITION OF ptif_test
+ FOR VALUES FROM (100) TO (maxvalue);
+INSERT INTO ptif_test SELECT i, 1 FROM generate_series(-100, 200) AS i;
+-- all tables in the tree with some size information
+SELECT relid::regclass, parentrelid::regclass, level, isleaf,
+ pg_relation_size(relid) = 0 AS is_empty
+ FROM pg_partition_tree('ptif_test'::regclass);
+ relid | parentrelid | level | isleaf | is_empty
+-------------+-------------+-------+--------+----------
+ ptif_test | | 0 | f | t
+ ptif_test0 | ptif_test | 1 | f | t
+ ptif_test1 | ptif_test | 1 | f | t
+ ptif_test2 | ptif_test | 1 | t | f
+ ptif_test01 | ptif_test0 | 2 | t | f
+ ptif_test11 | ptif_test1 | 2 | t | f
+(6 rows)
+
+-- passing an intermediate level partitioned table
+SELECT relid::regclass, parentrelid::regclass, level, isleaf
+ FROM pg_partition_tree('ptif_test0'::regclass) p
+ JOIN pg_class c ON (p.relid = c.oid);
+ relid | parentrelid | level | isleaf
+-------------+-------------+-------+--------
+ ptif_test0 | ptif_test | 0 | f
+ ptif_test01 | ptif_test0 | 1 | t
+(2 rows)
+
+-- passing a leaf partition
+SELECT relid::regclass, parentrelid::regclass, level, isleaf
+ FROM pg_partition_tree('ptif_test01'::regclass) p
+ JOIN pg_class c ON (p.relid = c.oid);
+ relid | parentrelid | level | isleaf
+-------------+-------------+-------+--------
+ ptif_test01 | ptif_test0 | 0 | t
+(1 row)
+
+-- this results in NULL, as there are no level 1 partitions of a leaf partition
+SELECT sum(pg_relation_size(relid)) AS total_size
+ FROM pg_partition_tree('ptif_test01'::regclass) WHERE level = 1;
+ total_size
+------------
+
+(1 row)
+
+DROP TABLE ptif_test;
+-- check that passing a table that's not part of any partition tree works
+-- the same as passing a leaf partition
+CREATE TABLE ptif_normal_table(a int);
+SELECT relid::regclass, parentrelid::regclass, level, isleaf
+ FROM pg_partition_tree('ptif_normal_table'::regclass) p
+ JOIN pg_class c ON (p.relid = c.oid);
+ relid | parentrelid | level | isleaf
+-------------------+-------------+-------+--------
+ ptif_normal_table | | 0 | t
+(1 row)
+
+DROP TABLE ptif_normal_table;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 16f979c8d9..6cb820bbc4 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -116,7 +116,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare without_oid c
# ----------
# Another group of parallel tests
# ----------
-test: identity partition_join partition_prune reloptions hash_part indexing partition_aggregate
+test: identity partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info
# event triggers cannot run concurrently with any test that runs DDL
test: event_trigger
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 42632be675..7e374c2daa 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -188,6 +188,7 @@ test: reloptions
test: hash_part
test: indexing
test: partition_aggregate
+test: partition_info
test: event_trigger
test: fast_default
test: stats
diff --git a/src/test/regress/sql/partition_info.sql b/src/test/regress/sql/partition_info.sql
new file mode 100644
index 0000000000..28f1a5e4a6
--- /dev/null
+++ b/src/test/regress/sql/partition_info.sql
@@ -0,0 +1,42 @@
+--
+-- Tests for pg_partition_tree
+--
+CREATE TABLE ptif_test (a int, b int) PARTITION BY range (a);
+CREATE TABLE ptif_test0 PARTITION OF ptif_test
+ FOR VALUES FROM (minvalue) TO (0) PARTITION BY list (b);
+CREATE TABLE ptif_test01 PARTITION OF ptif_test0 FOR VALUES IN (1);
+CREATE TABLE ptif_test1 PARTITION OF ptif_test
+ FOR VALUES FROM (0) TO (100) PARTITION BY list (b);
+CREATE TABLE ptif_test11 PARTITION OF ptif_test1 FOR VALUES IN (1);
+CREATE TABLE ptif_test2 PARTITION OF ptif_test
+ FOR VALUES FROM (100) TO (maxvalue);
+INSERT INTO ptif_test SELECT i, 1 FROM generate_series(-100, 200) AS i;
+
+-- all tables in the tree with some size information
+SELECT relid::regclass, parentrelid::regclass, level, isleaf,
+ pg_relation_size(relid) = 0 AS is_empty
+ FROM pg_partition_tree('ptif_test'::regclass);
+
+-- passing an intermediate level partitioned table
+SELECT relid::regclass, parentrelid::regclass, level, isleaf
+ FROM pg_partition_tree('ptif_test0'::regclass) p
+ JOIN pg_class c ON (p.relid = c.oid);
+
+-- passing a leaf partition
+SELECT relid::regclass, parentrelid::regclass, level, isleaf
+ FROM pg_partition_tree('ptif_test01'::regclass) p
+ JOIN pg_class c ON (p.relid = c.oid);
+
+-- this results in NULL, as there are no level 1 partitions of a leaf partition
+SELECT sum(pg_relation_size(relid)) AS total_size
+ FROM pg_partition_tree('ptif_test01'::regclass) WHERE level = 1;
+
+DROP TABLE ptif_test;
+
+-- check that passing a table that's not part of any partition tree works
+-- the same as passing a leaf partition
+CREATE TABLE ptif_normal_table(a int);
+SELECT relid::regclass, parentrelid::regclass, level, isleaf
+ FROM pg_partition_tree('ptif_normal_table'::regclass) p
+ JOIN pg_class c ON (p.relid = c.oid);
+DROP TABLE ptif_normal_table;
--
2.19.0
[application/pgp-signature] signature.asc (833B, ../../[email protected]/3-signature.asc)
download
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-05 06:05 Pavel Stehule <[email protected]>
parent: Michael Paquier <[email protected]>
1 sibling, 0 replies; 58+ messages in thread
From: Pavel Stehule @ 2018-10-05 06:05 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: Amit Langote <[email protected]>; Jesper Pedersen <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
pá 5. 10. 2018 v 7:57 odesílatel Michael Paquier <[email protected]>
napsal:
> On Thu, Oct 04, 2018 at 05:18:07PM +0900, Michael Paquier wrote:
> > So it seems that I am clearly outvoted here ;)
> >
> > Okay, let's do as you folks propose.
>
> And attached is a newer version with this isleaf stuff and the previous
> feedback from Amit integrated, as long as I recall about it. The
> version is indented, and tutti-quanti. Does that look fine to everybody
> here?
>
looks well
Pavel
> The CF bot should normally pick up this patch, the previous garbage seen
> in one of the tests seems to come from the previous implementation which
> used strings...
> --
> Michael
>
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-05 06:31 Amit Langote <[email protected]>
parent: Michael Paquier <[email protected]>
1 sibling, 1 reply; 58+ messages in thread
From: Amit Langote @ 2018-10-05 06:31 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: Jesper Pedersen <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On 2018/10/05 14:56, Michael Paquier wrote:
> On Thu, Oct 04, 2018 at 05:18:07PM +0900, Michael Paquier wrote:
>> So it seems that I am clearly outvoted here ;)
>>
>> Okay, let's do as you folks propose.
>
> And attached is a newer version with this isleaf stuff and the previous
> feedback from Amit integrated, as long as I recall about it. The
> version is indented, and tutti-quanti. Does that look fine to everybody
> here?
Thanks for making those changes yourself and posting the new version.
Can you check the attached diff file for some updates to the documentation
part of the patch. Other parts look fine.
Thanks,
Amit
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index be315aaabb..826c59ecd9 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -20209,14 +20209,14 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
<entry><literal><function>pg_partition_tree(<type>oid</type>)</function></literal></entry>
<entry><type>setof record</type></entry>
<entry>
- List information about table in a partition tree for a given
+ List information about tables in partition tree for a given
partitioned table, which consists of one row for each partition and
table itself. Information provided includes the OID of the partition,
- the OID of its immediate parent, if the partition is a leaf and its
- level in the hierarchy. The value of level begins at
- <literal>0</literal> for the input table in its role as the root of
- the partition tree, <literal>1</literal> for its partitions,
- <literal>2</literal> for their partitions, and so on.
+ the OID of its immediate parent, a boolean value telling if the
+ partition is a leaf, and an integer telling its level in the hierarchy.
+ The value of level begins at <literal>0</literal> for the input table
+ in its role as the root of the partition tree, <literal>1</literal> for
+ its partitions, <literal>2</literal> for their partitions, and so on.
</entry>
</row>
</tbody>
Attachments:
[text/plain] doc-fixes.diff (1.5K, ../../[email protected]/2-doc-fixes.diff)
download | inline diff:
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index be315aaabb..826c59ecd9 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -20209,14 +20209,14 @@ postgres=# SELECT * FROM pg_walfile_name_offset(pg_stop_backup());
<entry><literal><function>pg_partition_tree(<type>oid</type>)</function></literal></entry>
<entry><type>setof record</type></entry>
<entry>
- List information about table in a partition tree for a given
+ List information about tables in partition tree for a given
partitioned table, which consists of one row for each partition and
table itself. Information provided includes the OID of the partition,
- the OID of its immediate parent, if the partition is a leaf and its
- level in the hierarchy. The value of level begins at
- <literal>0</literal> for the input table in its role as the root of
- the partition tree, <literal>1</literal> for its partitions,
- <literal>2</literal> for their partitions, and so on.
+ the OID of its immediate parent, a boolean value telling if the
+ partition is a leaf, and an integer telling its level in the hierarchy.
+ The value of level begins at <literal>0</literal> for the input table
+ in its role as the root of the partition tree, <literal>1</literal> for
+ its partitions, <literal>2</literal> for their partitions, and so on.
</entry>
</row>
</tbody>
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-05 06:52 Michael Paquier <[email protected]>
parent: Amit Langote <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Michael Paquier @ 2018-10-05 06:52 UTC (permalink / raw)
To: Amit Langote <[email protected]>; +Cc: Jesper Pedersen <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On Fri, Oct 05, 2018 at 03:31:49PM +0900, Amit Langote wrote:
> Thanks for making those changes yourself and posting the new version.
>
> Can you check the attached diff file for some updates to the documentation
> part of the patch. Other parts look fine.
OK, I merged that into my local branch. From what I can see Mr Robot is
happy as well:
http://cfbot.cputube.org/amit-langote.html
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-05 12:22 Jesper Pedersen <[email protected]>
parent: Michael Paquier <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Jesper Pedersen @ 2018-10-05 12:22 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; Amit Langote <[email protected]>; +Cc: Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On 10/5/18 2:52 AM, Michael Paquier wrote:
> On Fri, Oct 05, 2018 at 03:31:49PM +0900, Amit Langote wrote:
>> Thanks for making those changes yourself and posting the new version.
>>
>> Can you check the attached diff file for some updates to the documentation
>> part of the patch. Other parts look fine.
>
> OK, I merged that into my local branch. From what I can see Mr Robot is
> happy as well:
> http://cfbot.cputube.org/amit-langote.html
Looks good.
Best regards,
Jesper
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-06 06:26 Michael Paquier <[email protected]>
parent: Jesper Pedersen <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Michael Paquier @ 2018-10-06 06:26 UTC (permalink / raw)
To: Jesper Pedersen <[email protected]>; +Cc: Amit Langote <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On Fri, Oct 05, 2018 at 08:22:49AM -0400, Jesper Pedersen wrote:
> Looks good.
Actually, after sleeping on it, there could be potentially two problems:
1) We don't check the relkind of the relation. For example it is
possible to get a tree from an index, which is incorrect. I would
suggest to restrain the root relation to be either a relation, a
partitioned table or a foreign table.
2) What about the users who can have a look at a tree? Shouldn't we
tighten that a bit more? I am sure it is not fine to allow any user to
look at what a partition tree looks like, hence only the owner of the
root should be able to look at its tree, no?
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-09 08:11 Amit Langote <[email protected]>
parent: Michael Paquier <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Amit Langote @ 2018-10-09 08:11 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; Jesper Pedersen <[email protected]>; +Cc: Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On 2018/10/06 15:26, Michael Paquier wrote:
> On Fri, Oct 05, 2018 at 08:22:49AM -0400, Jesper Pedersen wrote:
>> Looks good.
>
> Actually, after sleeping on it, there could be potentially two problems:
> 1) We don't check the relkind of the relation. For example it is
> possible to get a tree from an index, which is incorrect. I would
> suggest to restrain the root relation to be either a relation, a
> partitioned table or a foreign table.
Hmm, how would one find the size of a partitioned index tree if we don't
allow indexes to be passed?
> 2) What about the users who can have a look at a tree? Shouldn't we
> tighten that a bit more? I am sure it is not fine to allow any user to
> look at what a partition tree looks like, hence only the owner of the
> root should be able to look at its tree, no?
Maybe, we should apply same rules as are used when describing a table or
when getting its metadata via other means (pg_relation_size, etc.)?
Afaics, there are no checks performed in that case:
create table foo (a int primary key);
create user mt;
revoke all on foo from mt;
set session authorization mt;
\d foo
Table "public.foo"
Column │ Type │ Collation │ Nullable │ Default
────────┼─────────┼───────────┼──────────┼─────────
a │ integer │ │ not null │
Indexes:
"foo_pkey" PRIMARY KEY, btree (a)
select pg_relation_size('foo');
pg_relation_size
──────────────────
0
(1 row)
Should we do anything different in this case?
Thanks,
Amit
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-09 09:10 Michael Paquier <[email protected]>
parent: Amit Langote <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Michael Paquier @ 2018-10-09 09:10 UTC (permalink / raw)
To: Amit Langote <[email protected]>; +Cc: Jesper Pedersen <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On Tue, Oct 09, 2018 at 05:11:59PM +0900, Amit Langote wrote:
> Hmm, how would one find the size of a partitioned index tree if we don't
> allow indexes to be passed?
pg_total_relation_size() and pg_indexes_size() are designed for that
purpose. Anyway, the elements of a partition tree are things which can
be directly attached to it, like a table or a foreign table, and not
objects which are directly dependent on a given table, like indexes or
toast tables. So we have to add some filtering on the relkind.
Indexes, partitioned indexes and sequences are three things which cannot
be added as partitions. But I think that we had better just make sure
that the filtering allows RELKIND_RELATION, RELKIND_PARTITIONED_TABLE
and RELKIND_FOREIGN_TABLE relations only.
> Maybe, we should apply same rules as are used when describing a table or
> when getting its metadata via other means (pg_relation_size, etc.)?
> Afaics, there are no checks performed in that case:
>
> Should we do anything different in this case?
Yeah, perhaps we could live without any restriction. There would be
still time to tweak that behavior in the v12 development cycle if there
are any complaints.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-09 09:41 Amit Langote <[email protected]>
parent: Michael Paquier <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Amit Langote @ 2018-10-09 09:41 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: Jesper Pedersen <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On 2018/10/09 18:10, Michael Paquier wrote:
> On Tue, Oct 09, 2018 at 05:11:59PM +0900, Amit Langote wrote:
>> Hmm, how would one find the size of a partitioned index tree if we don't
>> allow indexes to be passed?
>
> pg_total_relation_size() and pg_indexes_size() are designed for that
> purpose. Anyway, the elements of a partition tree are things which can
> be directly attached to it, like a table or a foreign table, and not
> objects which are directly dependent on a given table, like indexes or
> toast tables. So we have to add some filtering on the relkind.
> Indexes, partitioned indexes and sequences are three things which cannot
> be added as partitions. But I think that we had better just make sure
> that the filtering allows RELKIND_RELATION, RELKIND_PARTITIONED_TABLE
> and RELKIND_FOREIGN_TABLE relations only.
I think the way I phrased my question may have been a bit confusing. I
was pointing out that just like tables, indexes can now also form their
own partition tree.
Partitioned indexes, just like partitioned tables, don't have their own
storage, so pg_relation_size() cannot be used to obtain their size. We
decided that the correct way to get the partitioned table's size is *not*
to modify pg_relation_size itself to internally find all partitions and
sum their sizes, but to provide a function that returns partitions and
tell users to write SQL queries around it to calculate the total size.
I'm saying that the new function should be able to be used with
partitioned indexes too.
Thanks,
Amit
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-09 10:05 Michael Paquier <[email protected]>
parent: Amit Langote <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Michael Paquier @ 2018-10-09 10:05 UTC (permalink / raw)
To: Amit Langote <[email protected]>; +Cc: Jesper Pedersen <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On Tue, Oct 09, 2018 at 06:41:59PM +0900, Amit Langote wrote:
> Partitioned indexes, just like partitioned tables, don't have their own
> storage, so pg_relation_size() cannot be used to obtain their size. We
> decided that the correct way to get the partitioned table's size is *not*
> to modify pg_relation_size itself to internally find all partitions and
> sum their sizes, but to provide a function that returns partitions and
> tell users to write SQL queries around it to calculate the total size.
> I'm saying that the new function should be able to be used with
> partitioned indexes too.
I have to admit that I find the concept non-intuitive. A partition tree
is a set of partitions based on a root called a partitioned table. A
partitioned index is not a partition, it is a specific object which is
associated to a partitioned table without any physical on-disk presence.
An index is not a partition as well, it is an object associated to one
relation.
I am not saying that there is no merit in that. I honestly don't know,
but being able to list all the partitioned tables and their partition
tables looks enough to cover all the grounds discussed, and there is no
need to work out more details for a new clone of find_all_inheritors and
get_partition_ancestors.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-09 10:20 Amit Langote <[email protected]>
parent: Michael Paquier <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Amit Langote @ 2018-10-09 10:20 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: Jesper Pedersen <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On 2018/10/09 19:05, Michael Paquier wrote:
> On Tue, Oct 09, 2018 at 06:41:59PM +0900, Amit Langote wrote:
>> Partitioned indexes, just like partitioned tables, don't have their own
>> storage, so pg_relation_size() cannot be used to obtain their size. We
>> decided that the correct way to get the partitioned table's size is *not*
>> to modify pg_relation_size itself to internally find all partitions and
>> sum their sizes, but to provide a function that returns partitions and
>> tell users to write SQL queries around it to calculate the total size.
>> I'm saying that the new function should be able to be used with
>> partitioned indexes too.
>
> I have to admit that I find the concept non-intuitive. A partition tree
> is a set of partitions based on a root called a partitioned table. A
> partitioned index is not a partition, it is a specific object which is
> associated to a partitioned table without any physical on-disk presence.
> An index is not a partition as well, it is an object associated to one
> relation.
I see it as follows: a partitioned index *does* have partitions and the
partitions in that case are index objects, whereas, a partitioned table's
partitions are table objects. IOW, I see the word "partition" as
describing a relationship, not any specific object or kind of objects.
> I am not saying that there is no merit in that. I honestly don't know,
> but being able to list all the partitioned tables and their partition
> tables looks enough to cover all the grounds discussed, and there is no
> need to work out more details for a new clone of find_all_inheritors and
> get_partition_ancestors.
Sorry if I'm misunderstanding something, but why would we need a new
clone? If we don't change pg_partition_tree() to only accept tables
(regular/partitioned/foreign tables) as input, then the same code can work
for indexes as well. As long as we store partition relationship in
pg_inherits, same code should work for both.
Thanks,
Amit
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-09 11:17 Michael Paquier <[email protected]>
parent: Amit Langote <[email protected]>
0 siblings, 2 replies; 58+ messages in thread
From: Michael Paquier @ 2018-10-09 11:17 UTC (permalink / raw)
To: Amit Langote <[email protected]>; +Cc: Jesper Pedersen <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On Tue, Oct 09, 2018 at 07:20:40PM +0900, Amit Langote wrote:
> Sorry if I'm misunderstanding something, but why would we need a new
> clone? If we don't change pg_partition_tree() to only accept tables
> (regular/partitioned/foreign tables) as input, then the same code can work
> for indexes as well. As long as we store partition relationship in
> pg_inherits, same code should work for both.
Okay, I see what you are coming at. However, please note that even if I
can see the dependencies in pg_inherits for indexes, the patch still
needs some work I am afraid if your intention is to do so:
CREATE TABLE ptif_test (a int, b int) PARTITION BY range (a);
create index ptif_test_i on ptif_test (a);
CREATE TABLE ptif_test0 PARTITION OF ptif_test
FOR VALUES FROM (minvalue) TO (0) PARTITION BY list (b);
=# select relid::regclass, parentrelid::regclass from
pg_partition_tree('ptif_test'::regclass);
relid | parentrelid
------------+-------------
ptif_test | null
ptif_test0 | ptif_test
(2 rows)
=# select relid::regclass, parentrelid::regclass from
pg_partition_tree('ptif_test_i'::regclass);
relid | parentrelid
-------------+-------------
ptif_test_i | null
(1 row)
In this case I would have expected ptif_test0_a_idx to be listed, with
ptif_test_i as a parent.
isleaf is of course wrong if the input is a partitioned index, so more
regression tests to cover those cases would be nice.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-09 11:25 Michael Paquier <[email protected]>
parent: Michael Paquier <[email protected]>
1 sibling, 0 replies; 58+ messages in thread
From: Michael Paquier @ 2018-10-09 11:25 UTC (permalink / raw)
To: Amit Langote <[email protected]>; +Cc: Jesper Pedersen <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On Tue, Oct 09, 2018 at 08:17:20PM +0900, Michael Paquier wrote:
> isleaf is of course wrong if the input is a partitioned index, so more
> regression tests to cover those cases would be nice.
Also, the call to find_all_inheritors needs AccessShareLock... NoLock
is not secure.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-10 02:54 Amit Langote <[email protected]>
parent: Michael Paquier <[email protected]>
1 sibling, 1 reply; 58+ messages in thread
From: Amit Langote @ 2018-10-10 02:54 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: Jesper Pedersen <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On 2018/10/09 20:17, Michael Paquier wrote:
> On Tue, Oct 09, 2018 at 07:20:40PM +0900, Amit Langote wrote:
>> Sorry if I'm misunderstanding something, but why would we need a new
>> clone? If we don't change pg_partition_tree() to only accept tables
>> (regular/partitioned/foreign tables) as input, then the same code can work
>> for indexes as well. As long as we store partition relationship in
>> pg_inherits, same code should work for both.
>
> Okay, I see what you are coming at. However, please note that even if I
> can see the dependencies in pg_inherits for indexes, the patch still
> needs some work I am afraid if your intention is to do so:
> CREATE TABLE ptif_test (a int, b int) PARTITION BY range (a);
> create index ptif_test_i on ptif_test (a);
> CREATE TABLE ptif_test0 PARTITION OF ptif_test
> FOR VALUES FROM (minvalue) TO (0) PARTITION BY list (b);
>
> =# select relid::regclass, parentrelid::regclass from
> pg_partition_tree('ptif_test'::regclass);
> relid | parentrelid
> ------------+-------------
> ptif_test | null
> ptif_test0 | ptif_test
> (2 rows)
> =# select relid::regclass, parentrelid::regclass from
> pg_partition_tree('ptif_test_i'::regclass);
> relid | parentrelid
> -------------+-------------
> ptif_test_i | null
> (1 row)
>
> In this case I would have expected ptif_test0_a_idx to be listed, with
> ptif_test_i as a parent.
I was partly wrong in saying that we wouldn't need any changes to support
partitioned indexes here. Actually, the core function
find_inheritance_children wouldn't scan pg_inherits for us if we pass an
(partitioned) index to it, even if the index may have children. That's
because of this quick-return code at the beginning of it:
/*
* Can skip the scan if pg_class shows the relation has never had a
* subclass.
*/
if (!has_subclass(parentrelId))
return NIL;
It appears that we don't set relhassubclass for partitioned indexes (that
is, on parent indexes), so the above the test is useless for indexes.
Maybe, we need to start another thread to discuss whether we should be
manipulating relhassubclass for index partitioning (I'd brought this up in
the context of relispartition, btw [1]). I'm not immediately sure if
setting relhassubclass correctly for indexes will have applications beyond
this one, but it might be something we should let others know so that we
can hear more opinions.
For time being, I've modified that code as follows:
@@ -68,9 +70,11 @@ find_inheritance_children(Oid parentrelId, LOCKMODE
lockmode)
/*
* Can skip the scan if pg_class shows the relation has never had a
- * subclass.
+ * subclass. Since partitioned indexes never have their
+ * relhassubclass set, we cannot skip the scan based on that.
*/
- if (!has_subclass(parentrelId))
+ if (get_rel_relkind(parentrelId) != RELKIND_PARTITIONED_INDEX &&
+ !has_subclass(parentrelId))
return NIL;
> isleaf is of course wrong if the input is a partitioned index, so more
> regression tests to cover those cases would be nice.
Yeah, I fixed that:
@@ -111,7 +111,8 @@ pg_partition_tree(PG_FUNCTION_ARGS)
nulls[1] = true;
/* isleaf */
- values[2] = BoolGetDatum(relkind != RELKIND_PARTITIONED_TABLE);
+ values[2] = BoolGetDatum(relkind != RELKIND_PARTITIONED_TABLE &&
+ relkind != RELKIND_PARTITIONED_INDEX);
On 2018/10/09 20:25, Michael Paquier wrote:
> Also, the call to find_all_inheritors needs AccessShareLock... NoLock
> is not secure.
I had thought about that, but don't remember why I made up my mind about
not taking any lock here. Maybe we should take locks. Fixed.
Attached updated patch. I updated docs and tests to include partitioned
indexes.
Thanks,
Amit
[1]
https://www.postgresql.org/message-id/12085bc4-0bc6-0f3a-4c43-57fe0681772b%40lab.ntt.co.jp
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-10 04:01 Michael Paquier <[email protected]>
parent: Amit Langote <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Michael Paquier @ 2018-10-10 04:01 UTC (permalink / raw)
To: Amit Langote <[email protected]>; +Cc: Jesper Pedersen <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On Wed, Oct 10, 2018 at 11:54:48AM +0900, Amit Langote wrote:
> I was partly wrong in saying that we wouldn't need any changes to support
> partitioned indexes here. Actually, the core function
> find_inheritance_children wouldn't scan pg_inherits for us if we pass an
> (partitioned) index to it, even if the index may have children.
>
> It appears that we don't set relhassubclass for partitioned indexes (that
> is, on parent indexes), so the above the test is useless for indexes.
Aha, so that was it.
> Maybe, we need to start another thread to discuss whether we should be
> manipulating relhassubclass for index partitioning (I'd brought this up in
> the context of relispartition, btw [1]). I'm not immediately sure if
> setting relhassubclass correctly for indexes will have applications beyond
> this one, but it might be something we should let others know so that we
> can hear more opinions.
This reminds of https://postgr.es/m/[email protected],
which has resulted in relhaspkey being removed from pg_class with
f66e8bf8. I would much prefer if we actually removed it... Now
relhassubclass is more tricky than relhaspkey as it gets used as a
fast-exit path for a couple of things:
1) prepunion.c when planning for child relations.
2) plancat.c
2) analyze.c
3) rewriteHandler.c
> For time being, I've modified that code as follows:
>
> @@ -68,9 +70,11 @@ find_inheritance_children(Oid parentrelId, LOCKMODE
> lockmode)
>
> /*
> * Can skip the scan if pg_class shows the relation has never had a
> - * subclass.
> + * subclass. Since partitioned indexes never have their
> + * relhassubclass set, we cannot skip the scan based on that.
> */
> - if (!has_subclass(parentrelId))
> + if (get_rel_relkind(parentrelId) != RELKIND_PARTITIONED_INDEX &&
> + !has_subclass(parentrelId))
> return NIL;
I don't like living with this hack. What if we simply nuked this
fast-path exit all together? The only code path where this could
represent a performance issue is RelationBuildPartitionKey(), but we
expect a partitioned table to have children anyway, and there will be
few cases where partitioned tables have no partitions.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-12 16:39 Robert Haas <[email protected]>
parent: Michael Paquier <[email protected]>
2 siblings, 1 reply; 58+ messages in thread
From: Robert Haas @ 2018-10-12 16:39 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: Amit Langote <[email protected]>; Thomas Munro <[email protected]>; Jesper Pedersen <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On Tue, Oct 2, 2018 at 11:37 PM Michael Paquier <[email protected]> wrote:
> Putting the new function pg_partition_children() in partition.c is a
> bad idea I think. So instead I think that we should put that in a
> different location, say utils/adt/partitionfuncs.c.
>
> + TupleDescInitEntry(tupdesc, (AttrNumber) 1, "relid",
> + REGCLASSOID, -1, 0);
> + TupleDescInitEntry(tupdesc, (AttrNumber) 2, "parentid",
> + REGCLASSOID, -1, 0);
> REGCLASSOID is used mainly for casting, so instead let's use OIDOID like
> any other system function.
-1. REGCLASSOID is a lot easier for humans to read and can still be
joined to an OID column somewhere if needed.
I think we should be striving to use types like regclass more often in
system catalogs and views, not less often.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-12 16:42 Alvaro Herrera <[email protected]>
parent: Robert Haas <[email protected]>
0 siblings, 0 replies; 58+ messages in thread
From: Alvaro Herrera @ 2018-10-12 16:42 UTC (permalink / raw)
To: Robert Haas <[email protected]>; +Cc: Michael Paquier <[email protected]>; Amit Langote <[email protected]>; Thomas Munro <[email protected]>; Jesper Pedersen <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On 2018-Oct-12, Robert Haas wrote:
> I think we should be striving to use types like regclass more often in
> system catalogs and views, not less often.
+1
--
Álvaro Herrera https://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-19 04:05 Amit Langote <[email protected]>
parent: Michael Paquier <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Amit Langote @ 2018-10-19 04:05 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: Jesper Pedersen <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On 2018/10/10 13:01, Michael Paquier wrote:
> On Wed, Oct 10, 2018 at 11:54:48AM +0900, Amit Langote wrote:
>> I was partly wrong in saying that we wouldn't need any changes to support
>> partitioned indexes here. Actually, the core function
>> find_inheritance_children wouldn't scan pg_inherits for us if we pass an
>> (partitioned) index to it, even if the index may have children.
>>
>> It appears that we don't set relhassubclass for partitioned indexes (that
>> is, on parent indexes), so the above the test is useless for indexes.
>
> Aha, so that was it.
>
>> Maybe, we need to start another thread to discuss whether we should be
>> manipulating relhassubclass for index partitioning (I'd brought this up in
>> the context of relispartition, btw [1]). I'm not immediately sure if
>> setting relhassubclass correctly for indexes will have applications beyond
>> this one, but it might be something we should let others know so that we
>> can hear more opinions.
>
> This reminds of https://postgr.es/m/[email protected],
> which has resulted in relhaspkey being removed from pg_class with
> f66e8bf8. I would much prefer if we actually removed it... Now
> relhassubclass is more tricky than relhaspkey as it gets used as a
> fast-exit path for a couple of things:
> 1) prepunion.c when planning for child relations.
> 2) plancat.c
> 2) analyze.c
> 3) rewriteHandler.c
I'm concerned about the 1st one. Currently in expand_inherited_rtentry(),
checking relhassubclass allows us to short-circuit scanning pg_inherits to
find out if a table has children. Note that expand_inherited_rtentry() is
called for *all* queries that contain a table so that we correctly
identify tables that would require inheritance planning. So, removing
relhassubclass means a slight (?) degradation for *all* queries.
>> For time being, I've modified that code as follows:
>>
>> @@ -68,9 +70,11 @@ find_inheritance_children(Oid parentrelId, LOCKMODE
>> lockmode)
>>
>> /*
>> * Can skip the scan if pg_class shows the relation has never had a
>> - * subclass.
>> + * subclass. Since partitioned indexes never have their
>> + * relhassubclass set, we cannot skip the scan based on that.
>> */
>> - if (!has_subclass(parentrelId))
>> + if (get_rel_relkind(parentrelId) != RELKIND_PARTITIONED_INDEX &&
>> + !has_subclass(parentrelId))
>> return NIL;
>
> I don't like living with this hack. What if we simply nuked this
> fast-path exit all together? The only code path where this could
> represent a performance issue is RelationBuildPartitionKey(), but we
> expect a partitioned table to have children anyway, and there will be
> few cases where partitioned tables have no partitions.
As I said above, the price of removing relhassubclass might be a bit
steep. So, the other alternative I mentioned before is to set
relhassubclass correctly even for indexes if only for pg_partition_tree to
be able to use find_inheritance_children unchanged.
Thanks,
Amit
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-19 07:47 Michael Paquier <[email protected]>
parent: Amit Langote <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Michael Paquier @ 2018-10-19 07:47 UTC (permalink / raw)
To: Amit Langote <[email protected]>; +Cc: Jesper Pedersen <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On Fri, Oct 19, 2018 at 01:05:52PM +0900, Amit Langote wrote:
> As I said above, the price of removing relhassubclass might be a bit
> steep. So, the other alternative I mentioned before is to set
> relhassubclass correctly even for indexes if only for pg_partition_tree to
> be able to use find_inheritance_children unchanged.
Playing devil's advocate a bit more... Another alternative here would
be to remove the fast path using relhassubclass from
find_inheritance_children and instead have its callers check for it :)
Anyway, it seems that you are right here. Just setting relhassubclass
for partitioned indexes feels more natural with what's on HEAD now.
Even if I'd like to see all those hypothetical columns in pg_class go
away, that cannot happen without a close lookup at the performance
impact.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-19 09:55 Amit Langote <[email protected]>
parent: Michael Paquier <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Amit Langote @ 2018-10-19 09:55 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: Jesper Pedersen <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On 2018/10/19 16:47, Michael Paquier wrote:
> On Fri, Oct 19, 2018 at 01:05:52PM +0900, Amit Langote wrote:
>> As I said above, the price of removing relhassubclass might be a bit
>> steep. So, the other alternative I mentioned before is to set
>> relhassubclass correctly even for indexes if only for pg_partition_tree to
>> be able to use find_inheritance_children unchanged.
>
> Playing devil's advocate a bit more... Another alternative here would
> be to remove the fast path using relhassubclass from
> find_inheritance_children and instead have its callers check for it :)
Yeah, we could make it the responsibility of the callers of
find_all_inheritors and find_inheritance_children to check relhassubclass
as an optimization and remove any reference to relhassubclass from
pg_inherits.c. Although we can write such a patch, it seems like it'd be
bigger than the patch to ensure the correct value of relhassubclass for
indexes, which I just posted on the other thread [1].
> Anyway, it seems that you are right here. Just setting relhassubclass
> for partitioned indexes feels more natural with what's on HEAD now.
> Even if I'd like to see all those hypothetical columns in pg_class go
> away, that cannot happen without a close lookup at the performance
> impact.
Okay, I updated the patch on this thread.
Since the updated patch depends on the correct value of relhassubclass
being set for indexes, this patch should be applied on top of the other
patch. I've attached here both.
Another change I made is something Robert and Alvaro seem to agree about
-- to use regclass instead of oid type as input/output columns.
Thanks,
Amit
[1]
https://www.postgresql.org/message-id/85d50b48-1b59-ae6c-e984-dd0b2926be3c%40lab.ntt.co.jp
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-29 03:59 Michael Paquier <[email protected]>
parent: Amit Langote <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Michael Paquier @ 2018-10-29 03:59 UTC (permalink / raw)
To: Amit Langote <[email protected]>; +Cc: Jesper Pedersen <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On Fri, Oct 19, 2018 at 06:55:09PM +0900, Amit Langote wrote:
> Yeah, we could make it the responsibility of the callers of
> find_all_inheritors and find_inheritance_children to check relhassubclass
> as an optimization and remove any reference to relhassubclass from
> pg_inherits.c. Although we can write such a patch, it seems like it'd be
> bigger than the patch to ensure the correct value of relhassubclass for
> indexes, which I just posted on the other thread [1].
And what is present as patch 0001 on this thread has been committed as
55853d6, so we are good for this part.
>> Anyway, it seems that you are right here. Just setting relhassubclass
>> for partitioned indexes feels more natural with what's on HEAD now.
>> Even if I'd like to see all those hypothetical columns in pg_class go
>> away, that cannot happen without a close lookup at the performance
>> impact.
>
> Okay, I updated the patch on this thread.
Thanks for the new version.
> Since the updated patch depends on the correct value of relhassubclass
> being set for indexes, this patch should be applied on top of the other
> patch. I've attached here both.
- if (!has_subclass(parentrelId))
+ if (get_rel_relkind(parentrelId) != RELKIND_PARTITIONED_INDEX &&
+ !has_subclass(parentrelId))
return NIL;
You don't need this bit anymore, relhassubclass is now set for
partitioned indexes.
+ ereport(ERROR,
+ (errcode(ERRCODE_WRONG_OBJECT_TYPE),
+ errmsg("\"%s\" is not a table, a foreign table, or an index",
+ get_rel_name(rootrelid))));
Should this also list "partitioned tables and partitioned indexes"? The
style is heavy, but that maps with what pgstattuple does..
The tests should include also something for a leaf index when fed to
pg_partition_tree() (in order to control the index names you could just
attach an index to a partition after creating it, but I leave that up to
you).
+ <entry><literal><function>pg_partition_tree(<type>oid</type>)</function></literal></entry>
+ <entry><type>setof record</type></entry>
The change to regclass has not been reflected yet in the documentation
and the implementation, because...
> Another change I made is something Robert and Alvaro seem to agree about
> -- to use regclass instead of oid type as input/output columns.
... I am in minority here, it feels lonely ;)
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-29 07:08 Amit Langote <[email protected]>
parent: Michael Paquier <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Amit Langote @ 2018-10-29 07:08 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: Jesper Pedersen <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On 2018/10/29 12:59, Michael Paquier wrote:
> On Fri, Oct 19, 2018 at 06:55:09PM +0900, Amit Langote wrote:
>> Yeah, we could make it the responsibility of the callers of
>> find_all_inheritors and find_inheritance_children to check relhassubclass
>> as an optimization and remove any reference to relhassubclass from
>> pg_inherits.c. Although we can write such a patch, it seems like it'd be
>> bigger than the patch to ensure the correct value of relhassubclass for
>> indexes, which I just posted on the other thread [1].
>
> And what is present as patch 0001 on this thread has been committed as
> 55853d6, so we are good for this part.
Thank you for that. :)
>>> Anyway, it seems that you are right here. Just setting relhassubclass
>>> for partitioned indexes feels more natural with what's on HEAD now.
>>> Even if I'd like to see all those hypothetical columns in pg_class go
>>> away, that cannot happen without a close lookup at the performance
>>> impact.
>>
>> Okay, I updated the patch on this thread.
>
> Thanks for the new version.
>
>> Since the updated patch depends on the correct value of relhassubclass
>> being set for indexes, this patch should be applied on top of the other
>> patch. I've attached here both.
>
> - if (!has_subclass(parentrelId))
> + if (get_rel_relkind(parentrelId) != RELKIND_PARTITIONED_INDEX &&
> + !has_subclass(parentrelId))
> return NIL;
>
> You don't need this bit anymore, relhassubclass is now set for
> partitioned indexes.
Oh, how did I forget to do that! Removed.
> + ereport(ERROR,
> + (errcode(ERRCODE_WRONG_OBJECT_TYPE),
> + errmsg("\"%s\" is not a table, a foreign table, or an index",
> + get_rel_name(rootrelid))));
> Should this also list "partitioned tables and partitioned indexes"? The
> style is heavy, but that maps with what pgstattuple does..
Hmm, I think we mention the word "partitioned" in the error message only
if partitioning is required to perform an operation but it's absent (for
example, trying to attach partition to a non-partitioned table) or if its
presence prevents certain operation from being performed (for example,
calling pgrowlocks() on a partitioned table). Neither seems true in this
case. One can pass a relation of any of the types mentioned in the above
error message to pg_partition_tree and get some output from it.
> The tests should include also something for a leaf index when fed to
> pg_partition_tree() (in order to control the index names you could just
> attach an index to a partition after creating it, but I leave that up to
> you).
>
> + <entry><literal><function>pg_partition_tree(<type>oid</type>)</function></literal></entry>
> + <entry><type>setof record</type></entry>
>
> The change to regclass has not been reflected yet in the documentation
> and the implementation, because...
>
>> Another change I made is something Robert and Alvaro seem to agree about
>> -- to use regclass instead of oid type as input/output columns.
>
> ... I am in minority here, it feels lonely ;)
I've fixed the documentation to mention regclass as the input type. Also,
I've also modified tests to not use ::regclass.
Thanks,
Amit
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-30 01:33 Michael Paquier <[email protected]>
parent: Amit Langote <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Michael Paquier @ 2018-10-30 01:33 UTC (permalink / raw)
To: Amit Langote <[email protected]>; +Cc: Jesper Pedersen <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On Mon, Oct 29, 2018 at 04:08:09PM +0900, Amit Langote wrote:
> Hmm, I think we mention the word "partitioned" in the error message only
> if partitioning is required to perform an operation but it's absent (for
> example, trying to attach partition to a non-partitioned table) or if its
> presence prevents certain operation from being performed (for example,
> calling pgrowlocks() on a partitioned table). Neither seems true in this
> case. One can pass a relation of any of the types mentioned in the above
> error message to pg_partition_tree and get some output from it.
Okay.. We could always reword the error message if there are any
complaints.
> I've fixed the documentation to mention regclass as the input type. Also,
> I've also modified tests to not use ::regclass.
Thanks for the new version and using better index names. I have
reviewed and committed the patch, with a couple of things tweaked:
- removal of the tests on the size, they don't seem useful as part of
showing partition information.
- no need to join on pg_class for the test of the normal table.
- added some comments and reformulated some other comments.
- added a test with a materialized view.
- added a test with a NULL input (as that's a strict function).
--
Michael
Attachments:
[application/pgp-signature] signature.asc (833B, ../../[email protected]/2-signature.asc)
download
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: partition tree inspection functions
@ 2018-10-30 01:53 Amit Langote <[email protected]>
parent: Michael Paquier <[email protected]>
0 siblings, 0 replies; 58+ messages in thread
From: Amit Langote @ 2018-10-30 01:53 UTC (permalink / raw)
To: Michael Paquier <[email protected]>; +Cc: Jesper Pedersen <[email protected]>; Thomas Munro <[email protected]>; Robert Haas <[email protected]>; Jeevan Ladhe <[email protected]>; pgsql-hackers
On 2018/10/30 10:33, Michael Paquier wrote:
> Thanks for the new version and using better index names. I have
> reviewed and committed the patch, with a couple of things tweaked:
> - removal of the tests on the size, they don't seem useful as part of
> showing partition information.
> - no need to join on pg_class for the test of the normal table.
> - added some comments and reformulated some other comments.
> - added a test with a materialized view.
> - added a test with a NULL input (as that's a strict function).
Thank you!
Regards,
Amit
^ permalink raw reply [nested|flat] 58+ 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; 58+ 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] 58+ 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; 58+ 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] 58+ 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; 58+ 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] 58+ 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; 58+ 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] 58+ 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; 58+ 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] 58+ 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; 58+ 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] 58+ 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; 58+ 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] 58+ 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; 58+ 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] 58+ messages in thread
* [PATCH v10 4/4] introduce restore_library
@ 2023-01-23 19:47 Nathan Bossart <[email protected]>
0 siblings, 0 replies; 58+ 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] 58+ messages in thread
* [PATCH v18 5/5] introduce restore_library
@ 2023-02-16 06:06 Nathan Bossart <[email protected]>
0 siblings, 0 replies; 58+ 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] 58+ messages in thread
* [PATCH v19 5/5] introduce restore_library
@ 2023-02-16 06:06 Nathan Bossart <[email protected]>
0 siblings, 0 replies; 58+ 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] 58+ messages in thread
* [PATCH v18 5/5] introduce restore_library
@ 2023-02-16 06:06 Nathan Bossart <[email protected]>
0 siblings, 0 replies; 58+ 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] 58+ messages in thread
* [PATCH v14 7/7] introduce restore_library
@ 2023-02-16 06:06 Nathan Bossart <[email protected]>
0 siblings, 0 replies; 58+ 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] 58+ messages in thread
* [PATCH v13 7/7] introduce restore_library
@ 2023-02-16 06:06 Nathan Bossart <[email protected]>
0 siblings, 0 replies; 58+ 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] 58+ messages in thread
* [PATCH v12 6/6] introduce restore_library
@ 2023-02-16 06:06 Nathan Bossart <[email protected]>
0 siblings, 0 replies; 58+ 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] 58+ messages in thread
* [PATCH v15 7/7] introduce restore_library
@ 2023-02-16 06:06 Nathan Bossart <[email protected]>
0 siblings, 0 replies; 58+ 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] 58+ messages in thread
* [PATCH v20 5/5] introduce restore_library
@ 2023-02-16 06:06 Nathan Bossart <[email protected]>
0 siblings, 0 replies; 58+ 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] 58+ messages in thread
* [PATCH v19 5/5] introduce restore_library
@ 2023-02-16 06:06 Nathan Bossart <[email protected]>
0 siblings, 0 replies; 58+ 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] 58+ messages in thread
* [PATCH v18 5/5] introduce restore_library
@ 2023-02-16 06:06 Nathan Bossart <[email protected]>
0 siblings, 0 replies; 58+ 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] 58+ messages in thread
* [PATCH v16 5/5] introduce restore_library
@ 2023-02-16 06:06 Nathan Bossart <[email protected]>
0 siblings, 0 replies; 58+ 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] 58+ messages in thread
* [PATCH v19 5/5] introduce restore_library
@ 2023-02-16 06:06 Nathan Bossart <[email protected]>
0 siblings, 0 replies; 58+ 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] 58+ messages in thread
* [PATCH v20 5/5] introduce restore_library
@ 2023-02-16 06:06 Nathan Bossart <[email protected]>
0 siblings, 0 replies; 58+ 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] 58+ messages in thread
* Re: POC: GROUP BY optimization
@ 2023-07-24 13:11 Tomas Vondra <[email protected]>
0 siblings, 1 reply; 58+ messages in thread
From: Tomas Vondra @ 2023-07-24 13:11 UTC (permalink / raw)
To: Andrey Lepikhov <[email protected]>; PostgreSQL Developers <[email protected]>; Tom Lane <[email protected]>; +Cc: Zhihong Yu <[email protected]>; Teodor Sigaev <[email protected]>; Tomas Vondra <[email protected]>; David Rowley <[email protected]>; a.rybakina <[email protected]>; Белялов Дамир Наилевич <[email protected]>
On 7/24/23 14:04, Andrey Lepikhov wrote:
> On 24/7/2023 16:56, Tomas Vondra wrote:
>> On 7/24/23 04:10, Andrey Lepikhov wrote:
>>> On 20/7/2023 18:46, Tomas Vondra wrote:
>>>> On 7/20/23 08:37, Andrey Lepikhov wrote:
>>>>> On 3/10/2022 21:56, Tom Lane wrote:
>>>>>> Revert "Optimize order of GROUP BY keys".
>>>>>>
>>>>>> This reverts commit db0d67db2401eb6238ccc04c6407a4fd4f985832 and
>>>>>> several follow-on fixes.
>>>>>> ...
>>>>>> Since we're hard up against the release deadline for v15, let's
>>>>>> revert these changes for now. We can always try again later.
>>>>>
>>>>> It may be time to restart the project. As a first step, I rebased the
>>>>> patch on the current master. It wasn't trivial because of some latest
>>>>> optimizations (a29eab, 1349d27 and 8d83a5d).
>>>>> Now, Let's repeat the review and rewrite the current path according to
>>>>> the reasons uttered in the revert commit.
>>>>
>>>> I think the fundamental task is to make the costing more reliable, and
>>>> the commit message 443df6e2db points out a couple challenges in this
>>>> area. Not sure how feasible it is to address enough of them ...
>>>>
>>>> 1) procost = 1.0 - I guess we could make this more realistic by doing
>>>> some microbenchmarks and tuning the costs for the most expensive cases.
>>>>
>>>> 2) estimating quicksort comparisons - This relies on ndistinct
>>>> estimates, and I'm not sure how much more reliable we can make those.
>>>> Probably not much :-( Not sure what to do about this, the only thing I
>>>> can think of is to track "reliability" of the estimates and only do the
>>>> reordering if we have high confidence in the estimates. That means
>>>> we'll
>>>> miss some optimization opportunities, but it should limit the risk.
>>> I read up on the history of this thread.
>>> As I see, all the problems mentioned above can be beaten by excluding
>>> the new cost model at all. We can sort GROUP BY columns according to the
>>> 'ndistinct' value.
>>> I see the reason for introducing the cost model in [1]. The main
>>> supporting point here is that with this patch, people couldn't optimize
>>> the query by themselves, organizing the order of the columns in a more
>>> optimal way. But now we have at least the GUC to switch off the
>>> behaviour introduced here. Also, some extensions, like the well-known
>>> pg_hint_plan, can help with automation.
>>
>> I think the main concern is that if we reorder the group keys and get it
>> wrong, it's a regression. If that happens often (due to costing based on
>> poor stats), it's a problem. Yes, there's a GUC, but that's a rather
>> blunt instrument, unfortunately.
> I see. My point here is if the ndistinct of one column is much more than
> the ndistinct of another, it is more probable that this correlation will
> be kept in the grouping phase. Here we can get some regression, which
> can be overweighed by the possibility below.
I think the word "probable" hits what the problem is. Because what if it
isn't? Also, we don't actually need ndistinct for individual attributes,
but for the groups of attributes. Imagine you do
GROUP BY a, b
so you need to estimate whether to do (a,b) or (b,a). You need to calculate
ndistinct(a,b) / ndistinct(a)
and
ndistinct(b,a) / ndistinct(b)
And similarly for more grouping keys. This is why we have ndistinct
extended statistics, mostly.
>>
>>> So, how about committing of the undoubted part of the feature and
>>> working on the cost model in a new thread?
>>>
>>
>> But Tom's commit message says this:
>>
>> Worse, to arrive at estimates of the number of calls made to the
>> lower-order-column comparison functions, the code needs to make
>> estimates of the numbers of distinct values of multiple columns,
>> which are necessarily even less trustworthy than per-column stats.
>>
>> so I'm not sure this really counts as "undoubted".
> Don't try to estimate multiple columns - just sort columns according to
> the value of ndistinct as a heuristic.
Surely you realize how easy such simple heuristics can fail, right?
> I think we should estimate the number of values of multiple columns only
> if we have extended statistics on these columns. And this can extend the
> applicability of extended statistics.
>
I don't see why this should estimate ndistinct differently than every
other place. That'd certainly be surprising. I can be convinced, but
there needs to be sound justification why that's the right way to do
(i.e. why it's less likely to cause poor planning choices).
> The suggestion above is just an attempt to gather low-hanging fruit
> right now. If it is not applicable, we will go a long way.
>
I'm not sure this qualities as "low hanging fruit" - that generally
means simple changes with little risk. But you're using it for rather
simplistic heuristic that can easily misfire (I think). At least that's
my impression, I can be convinced otherwise.
regards
--
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
^ permalink raw reply [nested|flat] 58+ messages in thread
* Re: POC: GROUP BY optimization
@ 2023-09-13 05:21 Andrey Lepikhov <[email protected]>
parent: Tomas Vondra <[email protected]>
0 siblings, 0 replies; 58+ messages in thread
From: Andrey Lepikhov @ 2023-09-13 05:21 UTC (permalink / raw)
To: PostgreSQL Developers <[email protected]>; +Cc: Teodor Sigaev <[email protected]>; Tomas Vondra <[email protected]>; Tom Lane <[email protected]>; David Rowley <[email protected]>; a.rybakina <[email protected]>; Белялов Дамир Наилевич <[email protected]>; Etsuro Fujita <[email protected]>
Hi,
Here is the patch rebased on the current master. Also, I fixed some
minor slips and one static analyzer warning.
This is just for adding to the next commitfest and enforcing work with
this patch.
One extra difference in newly added postgres_fdw tests is caused by this
patch - see changes in the query plan in attachment.
--
regards,
Andrey Lepikhov
Postgres Professional
From 33953655c9ac3f9ec64b80c9f2a2ff38bd178745 Mon Sep 17 00:00:00 2001
From: "Andrey V. Lepikhov" <[email protected]>
Date: Wed, 13 Sep 2023 11:20:03 +0700
Subject: [PATCH] Explore alternative orderings of group-by pathkeys during
optimization.
When evaluating a query with a multi-column GROUP BY clause using sort,
the cost may depend heavily on the order in which the keys are compared when
building the groups. Grouping does not imply any ordering, so we can compare
the keys in arbitrary order, and a Hash Agg leverages this. But for Group Agg,
we simply compared keys in the order specified in the query. This commit
explores alternative ordering of the keys, trying to find a cheaper one.
In principle, we might generate grouping paths for all permutations of the keys
and leave the rest to the optimizer. But that might get very expensive, so we
try to pick only a couple interesting orderings based on both local and global
information.
When planning the grouping path, we explore statistics (number of distinct
values, cost of the comparison function) for the keys and reorder them
to minimize comparison costs. Intuitively, it may be better to perform more
expensive comparisons (for complex data types, etc.) last because maybe
the cheaper comparisons will be enough. Similarly, the higher the cardinality
of a key, the lower the probability we'll need to compare more keys. The patch
generates and costs various orderings, picking the cheapest ones.
The ordering of group keys may interact with other parts of the query, some of
which may not be known while planning the grouping. For example, there may be
an explicit ORDER BY clause or some other ordering-dependent operation higher up
in the query, and using the same ordering may allow using either incremental
sort or even eliminating the sort entirely.
The patch generates orderings and picks those, minimizing the comparison cost
(for various path keys), and then adds orderings that might be useful for
operations higher up in the plan (ORDER BY, etc.). Finally, it always keeps
the ordering specified in the query, assuming the user might have additional
insights.
This introduces a new GUC enable_group_by_reordering so that the optimization
may be disabled if needed.
---
.../postgres_fdw/expected/postgres_fdw.out | 36 +-
src/backend/optimizer/path/costsize.c | 363 ++++++++++-
src/backend/optimizer/path/equivclass.c | 13 +-
src/backend/optimizer/path/pathkeys.c | 602 ++++++++++++++++++
src/backend/optimizer/plan/planner.c | 465 ++++++++------
src/backend/optimizer/util/pathnode.c | 4 +-
src/backend/utils/adt/selfuncs.c | 38 +-
src/backend/utils/misc/guc_tables.c | 10 +
src/backend/utils/misc/postgresql.conf.sample | 1 +
src/include/nodes/pathnodes.h | 10 +
src/include/optimizer/cost.h | 4 +-
src/include/optimizer/paths.h | 7 +
src/include/utils/selfuncs.h | 5 +
src/test/regress/expected/aggregates.out | 244 ++++++-
.../regress/expected/incremental_sort.out | 2 +-
src/test/regress/expected/join.out | 51 +-
src/test/regress/expected/merge.out | 15 +-
.../regress/expected/partition_aggregate.out | 44 +-
src/test/regress/expected/partition_join.out | 75 +--
src/test/regress/expected/sysviews.out | 3 +-
src/test/regress/expected/union.out | 60 +-
src/test/regress/sql/aggregates.sql | 99 +++
src/test/regress/sql/incremental_sort.sql | 2 +-
23 files changed, 1771 insertions(+), 382 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 144c114d0f..63af7feabe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2319,18 +2319,21 @@ SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM
-- join with pseudoconstant quals
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1 AND CURRENT_USER = SESSION_USER) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------
Limit
Output: t1.c1, t2.c1, t1.c3
- -> Result
+ -> Sort
Output: t1.c1, t2.c1, t1.c3
- One-Time Filter: (CURRENT_USER = SESSION_USER)
- -> Foreign Scan
- Output: t1.c1, t1.c3, t2.c1
- Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
- Remote SQL: SELECT r1."C 1", r2."C 1", r1.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r1."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
-(9 rows)
+ Sort Key: t1.c3, t1.c1
+ -> Result
+ Output: t1.c1, t2.c1, t1.c3
+ One-Time Filter: (CURRENT_USER = SESSION_USER)
+ -> Foreign Scan
+ Output: t1.c1, t1.c3, t2.c1
+ Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
+ Remote SQL: SELECT r1."C 1", r2."C 1", r1.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r1."C 1"))))
+(12 rows)
-- non-Var items in targetlist of the nullable rel of a join preventing
-- push-down in some cases
@@ -9609,13 +9612,16 @@ SELECT t1.a,t2.b,t3.c FROM fprt1 t1 INNER JOIN fprt2 t2 ON (t1.a = t2.b) INNER J
-- left outer join + nullable clause
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.a,t2.b,t2.c FROM fprt1 t1 LEFT JOIN (SELECT * FROM fprt2 WHERE a < 10) t2 ON (t1.a = t2.b and t1.b = t2.a) WHERE t1.a < 10 ORDER BY 1,2,3;
- QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Foreign Scan
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
Output: t1.a, fprt2.b, fprt2.c
- Relations: (public.ftprt1_p1 t1) LEFT JOIN (public.ftprt2_p1 fprt2)
- Remote SQL: SELECT r5.a, r6.b, r6.c FROM (public.fprt1_p1 r5 LEFT JOIN public.fprt2_p1 r6 ON (((r5.a = r6.b)) AND ((r5.b = r6.a)) AND ((r6.a < 10)))) WHERE ((r5.a < 10)) ORDER BY r5.a ASC NULLS LAST, r6.b ASC NULLS LAST, r6.c ASC NULLS LAST
-(4 rows)
+ Sort Key: t1.a, fprt2.b, fprt2.c
+ -> Foreign Scan
+ Output: t1.a, fprt2.b, fprt2.c
+ Relations: (public.ftprt1_p1 t1) LEFT JOIN (public.ftprt2_p1 fprt2)
+ Remote SQL: SELECT r5.a, r6.b, r6.c FROM (public.fprt1_p1 r5 LEFT JOIN public.fprt2_p1 r6 ON (((r5.a = r6.b)) AND ((r5.b = r6.a)) AND ((r6.a < 10)))) WHERE ((r5.a < 10))
+(7 rows)
SELECT t1.a,t2.b,t2.c FROM fprt1 t1 LEFT JOIN (SELECT * FROM fprt2 WHERE a < 10) t2 ON (t1.a = t2.b and t1.b = t2.a) WHERE t1.a < 10 ORDER BY 1,2,3;
a | b | c
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index d6ceafd51c..6f11f679ba 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1816,6 +1816,327 @@ cost_recursive_union(Path *runion, Path *nrterm, Path *rterm)
rterm->pathtarget->width);
}
+/*
+ * is_fake_var
+ * Workaround for generate_append_tlist() which generates fake Vars with
+ * varno == 0, that will cause a fail of estimate_num_group() call
+ *
+ * XXX Ummm, why would estimate_num_group fail with this?
+ */
+static bool
+is_fake_var(Expr *expr)
+{
+ if (IsA(expr, RelabelType))
+ expr = (Expr *) ((RelabelType *) expr)->arg;
+
+ return (IsA(expr, Var) && ((Var *) expr)->varno == 0);
+}
+
+/*
+ * get_width_cost_multiplier
+ * Returns relative complexity of comparing two values based on its width.
+ * The idea behind is that the comparison becomes more expensive the longer the
+ * value is. Return value is in cpu_operator_cost units.
+ */
+static double
+get_width_cost_multiplier(PlannerInfo *root, Expr *expr)
+{
+ double width = -1.0; /* fake value */
+
+ if (IsA(expr, RelabelType))
+ expr = (Expr *) ((RelabelType *) expr)->arg;
+
+ /* Try to find actual stat in corresponding relation */
+ if (IsA(expr, Var))
+ {
+ Var *var = (Var *) expr;
+
+ if (var->varno > 0 && var->varno < root->simple_rel_array_size)
+ {
+ RelOptInfo *rel = root->simple_rel_array[var->varno];
+
+ if (rel != NULL &&
+ var->varattno >= rel->min_attr &&
+ var->varattno <= rel->max_attr)
+ {
+ int ndx = var->varattno - rel->min_attr;
+
+ if (rel->attr_widths[ndx] > 0)
+ width = rel->attr_widths[ndx];
+ }
+ }
+ }
+
+ /* Didn't find any actual stats, try using type width instead. */
+ if (width < 0.0)
+ {
+ Node *node = (Node *) expr;
+
+ width = get_typavgwidth(exprType(node), exprTypmod(node));
+ }
+
+ /*
+ * Values are passed as Datum type, so comparisons can't be cheaper than
+ * comparing a Datum value.
+ *
+ * FIXME I find this reasoning questionable. We may pass int2, and
+ * comparing it is probably a bit cheaper than comparing a bigint.
+ */
+ if (width <= sizeof(Datum))
+ return 1.0;
+
+ /*
+ * We consider the cost of a comparison not to be directly proportional to
+ * width of the argument, because widths of the arguments could be
+ * slightly different (we only know the average width for the whole
+ * column). So we use log16(width) as an estimate.
+ */
+ return 1.0 + 0.125 * LOG2(width / sizeof(Datum));
+}
+
+/*
+ * compute_cpu_sort_cost
+ * compute CPU cost of sort (i.e. in-memory)
+ *
+ * The main thing we need to calculate to estimate sort CPU costs is the number
+ * of calls to the comparator functions. The difficulty is that for multi-column
+ * sorts there may be different data types involved (for some of which the calls
+ * may be much more expensive). Furthermore, columns may have a very different
+ * number of distinct values - the higher the number, the fewer comparisons will
+ * be needed for the following columns.
+ *
+ * The algorithm is incremental - we add pathkeys one by one, and at each step we
+ * estimate the number of necessary comparisons (based on the number of distinct
+ * groups in the current pathkey prefix and the new pathkey), and the comparison
+ * costs (which is data type specific).
+ *
+ * Estimation of the number of comparisons is based on ideas from:
+ *
+ * "Quicksort Is Optimal", Robert Sedgewick, Jon Bentley, 2002
+ * [https://www.cs.princeton.edu/~rs/talks/QuicksortIsOptimal.pdf]
+ *
+ * In term of that paper, let N - number of tuples, Xi - number of identical
+ * tuples with value Ki, then the estimate of number of comparisons is:
+ *
+ * log(N! / (X1! * X2! * ..)) ~ sum(Xi * log(N/Xi))
+ *
+ * We assume all Xi the same because now we don't have any estimation of
+ * group sizes, we have only know the estimate of number of groups (distinct
+ * values). In that case, formula becomes:
+ *
+ * N * log(NumberOfGroups)
+ *
+ * For multi-column sorts we need to estimate the number of comparisons for
+ * each individual column - for example with columns (c1, c2, ..., ck) we
+ * can estimate that number of comparisons on ck is roughly
+ *
+ * ncomparisons(c1, c2, ..., ck) / ncomparisons(c1, c2, ..., c(k-1))
+ *
+ * Let k be a column number, Gk - number of groups defined by k columns, and Fk
+ * the cost of the comparison is
+ *
+ * N * sum( Fk * log(Gk) )
+ *
+ * Note: We also consider column width, not just the comparator cost.
+ *
+ * NOTE: some callers currently pass NIL for pathkeys because they
+ * can't conveniently supply the sort keys. In this case, it will fallback to
+ * simple comparison cost estimate.
+ */
+static Cost
+compute_cpu_sort_cost(PlannerInfo *root, List *pathkeys, int nPresortedKeys,
+ Cost comparison_cost, double tuples, double output_tuples,
+ bool heapSort)
+{
+ Cost per_tuple_cost = 0.0;
+ ListCell *lc;
+ List *pathkeyExprs = NIL;
+ double tuplesPerPrevGroup = tuples;
+ double totalFuncCost = 1.0;
+ bool has_fake_var = false;
+ int i = 0;
+ Oid prev_datatype = InvalidOid;
+ List *cache_varinfos = NIL;
+
+ /* fallback if pathkeys is unknown */
+ if (list_length(pathkeys) == 0)
+ {
+ /*
+ * If we'll use a bounded heap-sort keeping just K tuples in memory,
+ * for a total number of tuple comparisons of N log2 K; but the
+ * constant factor is a bit higher than for quicksort. Tweak it so
+ * that the cost curve is continuous at the crossover point.
+ */
+ output_tuples = (heapSort) ? 2.0 * output_tuples : tuples;
+ per_tuple_cost += 2.0 * cpu_operator_cost * LOG2(output_tuples);
+
+ /* add cost provided by caller */
+ per_tuple_cost += comparison_cost;
+
+ return per_tuple_cost * tuples;
+ }
+
+ /*
+ * Computing total cost of sorting takes into account the per-column
+ * comparison function cost. We try to compute the needed number of
+ * comparisons per column.
+ */
+ foreach(lc, pathkeys)
+ {
+ PathKey *pathkey = (PathKey *) lfirst(lc);
+ EquivalenceMember *em;
+ double nGroups,
+ correctedNGroups;
+ Cost funcCost = 1.0;
+
+ /*
+ * We believe that equivalence members aren't very different, so, to
+ * estimate cost we consider just the first member.
+ */
+ em = (EquivalenceMember *) linitial(pathkey->pk_eclass->ec_members);
+
+ if (em->em_datatype != InvalidOid)
+ {
+ /* do not lookup funcCost if the data type is the same */
+ if (prev_datatype != em->em_datatype)
+ {
+ Oid sortop;
+ QualCost cost;
+
+ sortop = get_opfamily_member(pathkey->pk_opfamily,
+ em->em_datatype, em->em_datatype,
+ pathkey->pk_strategy);
+
+ cost.startup = 0;
+ cost.per_tuple = 0;
+ add_function_cost(root, get_opcode(sortop), NULL, &cost);
+
+ /*
+ * add_function_cost returns the product of cpu_operator_cost
+ * and procost, but we need just procost, co undo that.
+ */
+ funcCost = cost.per_tuple / cpu_operator_cost;
+
+ prev_datatype = em->em_datatype;
+ }
+ }
+
+ /* factor in the width of the values in this column */
+ funcCost *= get_width_cost_multiplier(root, em->em_expr);
+
+ /* now we have per-key cost, so add to the running total */
+ totalFuncCost += funcCost;
+
+ /* remember if we have found a fake Var in pathkeys */
+ has_fake_var |= is_fake_var(em->em_expr);
+ pathkeyExprs = lappend(pathkeyExprs, em->em_expr);
+
+ /*
+ * We need to calculate the number of comparisons for this column,
+ * which requires knowing the group size. So we estimate the number of
+ * groups by calling estimate_num_groups_incremental(), which
+ * estimates the group size for "new" pathkeys.
+ *
+ * Note: estimate_num_groups_incremental does not handle fake Vars, so
+ * use a default estimate otherwise.
+ */
+ if (!has_fake_var)
+ nGroups = estimate_num_groups_incremental(root, pathkeyExprs,
+ tuplesPerPrevGroup, NULL, NULL,
+ &cache_varinfos,
+ list_length(pathkeyExprs) - 1);
+ else if (tuples > 4.0)
+
+ /*
+ * Use geometric mean as estimation if there are no stats.
+ *
+ * We don't use DEFAULT_NUM_DISTINCT here, because that's used for
+ * a single column, but here we're dealing with multiple columns.
+ */
+ nGroups = ceil(2.0 + sqrt(tuples) * (i + 1) / list_length(pathkeys));
+ else
+ nGroups = tuples;
+
+ /*
+ * Presorted keys are not considered in the cost above, but we still
+ * do have to compare them in the qsort comparator. So make sure to
+ * factor in the cost in that case.
+ */
+ if (i >= nPresortedKeys)
+ {
+ if (heapSort)
+ {
+ /*
+ * have to keep at least one group, and a multiple of group
+ * size
+ */
+ correctedNGroups = ceil(output_tuples / tuplesPerPrevGroup);
+ }
+ else
+ /* all groups in the input */
+ correctedNGroups = nGroups;
+
+ correctedNGroups = Max(1.0, ceil(correctedNGroups));
+
+ per_tuple_cost += totalFuncCost * LOG2(correctedNGroups);
+ }
+
+ i++;
+
+ /*
+ * Once we get single-row group, it means tuples in the group are
+ * unique and we can skip all remaining columns.
+ */
+ if (tuplesPerPrevGroup <= nGroups)
+ break;
+
+ /*
+ * Uniform distributions with all groups being of the same size are
+ * the best case, with nice smooth behavior. Real-world distributions
+ * tend not to be uniform, though, and we don't have any reliable
+ * easy-to-use information. As a basic defense against skewed
+ * distributions, we use a 1.5 factor to make the expected group a bit
+ * larger, but we need to be careful not to make the group larger than
+ * in the preceding step.
+ */
+ tuplesPerPrevGroup = Min(tuplesPerPrevGroup,
+ ceil(1.5 * tuplesPerPrevGroup / nGroups));
+ }
+
+ list_free(pathkeyExprs);
+
+ /* per_tuple_cost is in cpu_operator_cost units */
+ per_tuple_cost *= cpu_operator_cost;
+
+ /*
+ * Accordingly to "Introduction to algorithms", Thomas H. Cormen, Charles
+ * E. Leiserson, Ronald L. Rivest, ISBN 0-07-013143-0, quicksort
+ * estimation formula has additional term proportional to number of tuples
+ * (see Chapter 8.2 and Theorem 4.1). That affects cases with a low number
+ * of tuples, approximately less than 1e4. We could implement it as an
+ * additional multiplier under the logarithm, but we use a bit more
+ * complex formula which takes into account the number of unique tuples
+ * and it's not clear how to combine the multiplier with the number of
+ * groups. Estimate it as 10 cpu_operator_cost units.
+ */
+ per_tuple_cost += 10 * cpu_operator_cost;
+
+ per_tuple_cost += comparison_cost;
+
+ return tuples * per_tuple_cost;
+}
+
+/*
+ * simple wrapper just to estimate best sort path
+ */
+Cost
+cost_sort_estimate(PlannerInfo *root, List *pathkeys, int nPresortedKeys,
+ double tuples)
+{
+ return compute_cpu_sort_cost(root, pathkeys, nPresortedKeys,
+ 0, tuples, tuples, false);
+}
+
/*
* cost_tuplesort
* Determines and returns the cost of sorting a relation using tuplesort,
@@ -1832,7 +2153,7 @@ cost_recursive_union(Path *runion, Path *nrterm, Path *rterm)
* number of initial runs formed and M is the merge order used by tuplesort.c.
* Since the average initial run should be about sort_mem, we have
* disk traffic = 2 * relsize * ceil(logM(p / sort_mem))
- * cpu = comparison_cost * t * log2(t)
+ * and cpu cost (computed by compute_cpu_sort_cost()).
*
* If the sort is bounded (i.e., only the first k result tuples are needed)
* and k tuples can fit into sort_mem, we use a heap method that keeps only
@@ -1851,9 +2172,11 @@ cost_recursive_union(Path *runion, Path *nrterm, Path *rterm)
* 'comparison_cost' is the extra cost per comparison, if any
* 'sort_mem' is the number of kilobytes of work memory allowed for the sort
* 'limit_tuples' is the bound on the number of output tuples; -1 if no bound
+ * 'startup_cost' is expected to be 0 at input. If there is "input cost" it should
+ * be added by caller later
*/
static void
-cost_tuplesort(Cost *startup_cost, Cost *run_cost,
+cost_tuplesort(PlannerInfo *root, List *pathkeys, Cost *startup_cost, Cost *run_cost,
double tuples, int width,
Cost comparison_cost, int sort_mem,
double limit_tuples)
@@ -1870,9 +2193,6 @@ cost_tuplesort(Cost *startup_cost, Cost *run_cost,
if (tuples < 2.0)
tuples = 2.0;
- /* Include the default cost-per-comparison */
- comparison_cost += 2.0 * cpu_operator_cost;
-
/* Do we have a useful LIMIT? */
if (limit_tuples > 0 && limit_tuples < tuples)
{
@@ -1896,12 +2216,10 @@ cost_tuplesort(Cost *startup_cost, Cost *run_cost,
double log_runs;
double npageaccesses;
- /*
- * CPU costs
- *
- * Assume about N log2 N comparisons
- */
- *startup_cost = comparison_cost * tuples * LOG2(tuples);
+ /* CPU costs */
+ *startup_cost = compute_cpu_sort_cost(root, pathkeys, 0,
+ comparison_cost, tuples,
+ tuples, false);
/* Disk costs */
@@ -1917,18 +2235,17 @@ cost_tuplesort(Cost *startup_cost, Cost *run_cost,
}
else if (tuples > 2 * output_tuples || input_bytes > sort_mem_bytes)
{
- /*
- * We'll use a bounded heap-sort keeping just K tuples in memory, for
- * a total number of tuple comparisons of N log2 K; but the constant
- * factor is a bit higher than for quicksort. Tweak it so that the
- * cost curve is continuous at the crossover point.
- */
- *startup_cost = comparison_cost * tuples * LOG2(2.0 * output_tuples);
+ /* We'll use a bounded heap-sort keeping just K tuples in memory. */
+ *startup_cost = compute_cpu_sort_cost(root, pathkeys, 0,
+ comparison_cost, tuples,
+ output_tuples, true);
}
else
{
/* We'll use plain quicksort on all the input tuples */
- *startup_cost = comparison_cost * tuples * LOG2(tuples);
+ *startup_cost = compute_cpu_sort_cost(root, pathkeys, 0,
+ comparison_cost, tuples,
+ tuples, false);
}
/*
@@ -2042,7 +2359,7 @@ cost_incremental_sort(Path *path,
* Estimate the average cost of sorting of one group where presorted keys
* are equal.
*/
- cost_tuplesort(&group_startup_cost, &group_run_cost,
+ cost_tuplesort(root, pathkeys, &group_startup_cost, &group_run_cost,
group_tuples, width, comparison_cost, sort_mem,
limit_tuples);
@@ -2102,7 +2419,7 @@ cost_sort(Path *path, PlannerInfo *root,
Cost startup_cost;
Cost run_cost;
- cost_tuplesort(&startup_cost, &run_cost,
+ cost_tuplesort(root, pathkeys, &startup_cost, &run_cost,
tuples, width,
comparison_cost, sort_mem,
limit_tuples);
@@ -2200,7 +2517,7 @@ append_nonpartial_cost(List *subpaths, int numpaths, int parallel_workers)
* Determines and returns the cost of an Append node.
*/
void
-cost_append(AppendPath *apath)
+cost_append(AppendPath *apath, PlannerInfo *root)
{
ListCell *l;
@@ -2268,7 +2585,7 @@ cost_append(AppendPath *apath)
* any child.
*/
cost_sort(&sort_path,
- NULL, /* doesn't currently need root */
+ root,
pathkeys,
subpath->total_cost,
subpath->rows,
diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c
index 7fa502d6e2..07edd4f38e 100644
--- a/src/backend/optimizer/path/equivclass.c
+++ b/src/backend/optimizer/path/equivclass.c
@@ -652,7 +652,18 @@ get_eclass_for_sort_expr(PlannerInfo *root,
if (opcintype == cur_em->em_datatype &&
equal(expr, cur_em->em_expr))
- return cur_ec; /* Match! */
+ {
+ /*
+ * Match!
+ *
+ * Copy the sortref if it wasn't set yet. That may happen if
+ * the ec was constructed from WHERE clause, i.e. it doesn't
+ * have a target reference at all.
+ */
+ if (cur_ec->ec_sortref == 0 && sortref > 0)
+ cur_ec->ec_sortref = sortref;
+ return cur_ec;
+ }
}
}
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c
index fdb60aaa8d..2dfc983a6d 100644
--- a/src/backend/optimizer/path/pathkeys.c
+++ b/src/backend/optimizer/path/pathkeys.c
@@ -17,17 +17,24 @@
*/
#include "postgres.h"
+#include <float.h>
+
+#include "miscadmin.h"
#include "access/stratnum.h"
#include "catalog/pg_opfamily.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "nodes/plannodes.h"
+#include "optimizer/cost.h"
#include "optimizer/optimizer.h"
#include "optimizer/pathnode.h"
#include "optimizer/paths.h"
#include "partitioning/partbounds.h"
#include "utils/lsyscache.h"
+#include "utils/selfuncs.h"
+/* Consider reordering of GROUP BY keys? */
+bool enable_group_by_reordering = true;
static bool pathkey_is_redundant(PathKey *new_pathkey, List *pathkeys);
static bool matches_boolean_partition_clause(RestrictInfo *rinfo,
@@ -350,6 +357,548 @@ pathkeys_contained_in(List *keys1, List *keys2)
return false;
}
+/*
+ * group_keys_reorder_by_pathkeys
+ * Reorder GROUP BY keys to match pathkeys of input path.
+ *
+ * Function returns new lists (pathkeys and clauses), original GROUP BY lists
+ * stay untouched.
+ *
+ * Returns the number of GROUP BY keys with a matching pathkey.
+ */
+int
+group_keys_reorder_by_pathkeys(List *pathkeys, List **group_pathkeys,
+ List **group_clauses,
+ int num_groupby_pathkeys)
+{
+ List *new_group_pathkeys = NIL,
+ *new_group_clauses = NIL;
+ ListCell *lc;
+ int n;
+
+ if (pathkeys == NIL || *group_pathkeys == NIL)
+ return 0;
+
+ /*
+ * Walk the pathkeys (determining ordering of the input path) and see if
+ * there's a matching GROUP BY key. If we find one, we append it to the
+ * list, and do the same for the clauses.
+ *
+ * Once we find the first pathkey without a matching GROUP BY key, the
+ * rest of the pathkeys are useless and can't be used to evaluate the
+ * grouping, so we abort the loop and ignore the remaining pathkeys.
+ *
+ * XXX Pathkeys are built in a way to allow simply comparing pointers.
+ */
+ foreach(lc, pathkeys)
+ {
+ PathKey *pathkey = (PathKey *) lfirst(lc);
+ SortGroupClause *sgc;
+
+ if (foreach_current_index(lc) >= num_groupby_pathkeys ||
+ !list_member_ptr(*group_pathkeys, pathkey))
+ /* */
+ break;
+
+ /* abort on first mismatch */
+ sgc = get_sortgroupref_clause_noerr(pathkey->pk_eclass->ec_sortref,
+ *group_clauses);
+ if (!sgc)
+ /* The grouping clause is not cover this paathkey */
+ break;
+
+ new_group_pathkeys = lappend(new_group_pathkeys, pathkey);
+ new_group_clauses = lappend(new_group_clauses, sgc);
+ }
+
+ /* remember the number of pathkeys with a matching GROUP BY key */
+ n = list_length(new_group_pathkeys);
+
+ /* append the remaining group pathkeys (will be treated as not sorted) */
+ *group_pathkeys = list_concat_unique_ptr(new_group_pathkeys,
+ *group_pathkeys);
+ *group_clauses = list_concat_unique_ptr(new_group_clauses,
+ *group_clauses);
+
+ return n;
+}
+
+/*
+ * Used to generate all permutations of a pathkey list.
+ */
+typedef struct PathkeyMutatorState
+{
+ List *elemsList;
+ ListCell **elemCells;
+ void **elems;
+ int *positions;
+ int mutatorNColumns;
+ int count;
+} PathkeyMutatorState;
+
+
+/*
+ * PathkeyMutatorInit
+ * Initialize state of the permutation generator.
+ *
+ * We want to generate permutations of elements in the "elems" list. We may want
+ * to skip some number of elements at the beginning (when treating as presorted)
+ * or at the end (we only permute a limited number of group keys).
+ *
+ * The list is decomposed into elements, and we also keep pointers to individual
+ * cells. This allows us to build the permuted list quickly and cheaply, without
+ * creating any copies.
+ */
+static void
+PathkeyMutatorInit(PathkeyMutatorState *state, List *elems, int start, int end)
+{
+ int i;
+ int n = end - start;
+ ListCell *lc;
+
+ memset(state, 0, sizeof(*state));
+
+ state->mutatorNColumns = n;
+
+ state->elemsList = list_copy(elems);
+
+ state->elems = palloc(sizeof(void *) * n);
+ state->elemCells = palloc(sizeof(ListCell *) * n);
+ state->positions = palloc(sizeof(int) * n);
+
+ i = 0;
+ for_each_cell(lc, state->elemsList, list_nth_cell(state->elemsList, start))
+ {
+ state->elemCells[i] = lc;
+ state->elems[i] = lfirst(lc);
+ state->positions[i] = i + 1;
+ i++;
+
+ if (i >= n)
+ break;
+ }
+}
+
+/* Swap two elements of an array. */
+static void
+PathkeyMutatorSwap(int *a, int i, int j)
+{
+ int s = a[i];
+
+ a[i] = a[j];
+ a[j] = s;
+}
+
+/*
+ * Generate the next permutation of elements.
+ */
+static bool
+PathkeyMutatorNextSet(int *a, int n)
+{
+ int j,
+ k,
+ l,
+ r;
+
+ j = n - 2;
+
+ while (j >= 0 && a[j] >= a[j + 1])
+ j--;
+
+ if (j < 0)
+ return false;
+
+ k = n - 1;
+
+ while (k > 0 && a[j] >= a[k])
+ k--;
+
+ PathkeyMutatorSwap(a, j, k);
+
+ l = j + 1;
+ r = n - 1;
+
+ while (l < r)
+ PathkeyMutatorSwap(a, l++, r--);
+
+ return true;
+}
+
+/*
+ * PathkeyMutatorNext
+ * Generate the next permutation of list of elements.
+ *
+ * Returns the next permutation (as a list of elements) or NIL if there are no
+ * more permutations.
+ */
+static List *
+PathkeyMutatorNext(PathkeyMutatorState *state)
+{
+ int i;
+
+ state->count++;
+
+ /* first permutation is original list */
+ if (state->count == 1)
+ return state->elemsList;
+
+ /* when there are no more permutations, return NIL */
+ if (!PathkeyMutatorNextSet(state->positions, state->mutatorNColumns))
+ {
+ pfree(state->elems);
+ pfree(state->elemCells);
+ pfree(state->positions);
+
+ list_free(state->elemsList);
+
+ return NIL;
+ }
+
+ /* update the list cells to point to the right elements */
+ for (i = 0; i < state->mutatorNColumns; i++)
+ lfirst(state->elemCells[i]) =
+ (void *) state->elems[state->positions[i] - 1];
+
+ return state->elemsList;
+}
+
+/*
+ * Cost of comparing pathkeys.
+ */
+typedef struct PathkeySortCost
+{
+ Cost cost;
+ PathKey *pathkey;
+} PathkeySortCost;
+
+static int
+pathkey_sort_cost_comparator(const void *_a, const void *_b)
+{
+ const PathkeySortCost *a = (PathkeySortCost *) _a;
+ const PathkeySortCost *b = (PathkeySortCost *) _b;
+
+ if (a->cost < b->cost)
+ return -1;
+ else if (a->cost == b->cost)
+ return 0;
+ return 1;
+}
+
+/*
+ * get_cheapest_group_keys_order
+ * Reorders the group pathkeys / clauses to minimize the comparison cost.
+ *
+ * Given the list of pathkeys in '*group_pathkeys', we try to arrange these
+ * in an order that minimizes the sort costs that will be incurred by the
+ * GROUP BY. The costs mainly depend on the cost of the sort comparator
+ * function(s) and the number of distinct values in each column of the GROUP
+ * BY clause (*group_clauses). Sorting on subsequent columns is only required
+ * for tiebreak situations where two values sort equally.
+ *
+ * In case the input is partially sorted, only the remaining pathkeys are
+ * considered. 'n_preordered' denotes how many of the leading *group_pathkeys
+ * the input is presorted by.
+ *
+ * Returns true and sets *group_pathkeys and *group_clauses to the newly
+ * ordered versions of the lists that were passed in via these parameters.
+ * If no reordering was deemed necessary then we return false, in which case
+ * the *group_pathkeys and *group_clauses lists are left untouched. The
+ * original *group_pathkeys and *group_clauses parameter values are never
+ * destructively modified in place.
+ */
+static bool
+get_cheapest_group_keys_order(PlannerInfo *root, double nrows,
+ List **group_pathkeys, List **group_clauses,
+ int n_preordered)
+{
+ List *new_group_pathkeys = NIL;
+ List *new_group_clauses = NIL;
+ List *var_group_pathkeys;
+ ListCell *cell;
+ PathkeyMutatorState mstate;
+ double cheapest_sort_cost = DBL_MAX;
+ int nFreeKeys;
+ int nToPermute;
+
+ /* If there are less than 2 unsorted pathkeys, we're done. */
+ if (root->num_groupby_pathkeys - n_preordered < 2)
+ return false;
+
+ /*
+ * We could exhaustively cost all possible orderings of the pathkeys, but
+ * for a large number of pathkeys it might be prohibitively expensive. So
+ * we try to apply simple cheap heuristics first - we sort the pathkeys by
+ * sort cost (as if the pathkey was sorted independently) and then check
+ * only the four cheapest pathkeys. The remaining pathkeys are kept
+ * ordered by cost.
+ *
+ * XXX This is a very simple heuristics, but likely to work fine for most
+ * cases (because the number of GROUP BY clauses tends to be lower than
+ * 4). But it ignores how the number of distinct values in each pathkey
+ * affects the following steps. It might be better to use "more expensive"
+ * pathkey first if it has many distinct values, because it then limits
+ * the number of comparisons for the remaining pathkeys. But evaluating
+ * that is likely quite the expensive.
+ */
+ nFreeKeys = root->num_groupby_pathkeys - n_preordered;
+ nToPermute = 4;
+ if (nFreeKeys > nToPermute)
+ {
+ PathkeySortCost *costs = palloc(sizeof(PathkeySortCost) * nFreeKeys);
+ PathkeySortCost *cost = costs;
+ int i = 0;
+
+ /*
+ * Estimate cost for sorting individual pathkeys skipping the
+ * pre-ordered pathkeys.
+ */
+ for_each_from(cell, *group_pathkeys, n_preordered)
+ {
+ PathKey *pathkey = (PathKey *) lfirst(cell);
+ List *to_cost;
+
+ if (foreach_current_index(cell) >= root->num_groupby_pathkeys)
+ break;
+
+ i++;
+ to_cost = list_make1(pathkey);
+ cost->pathkey = pathkey;
+ cost->cost = cost_sort_estimate(root, to_cost, 0, nrows);
+ cost++;
+
+ list_free(to_cost);
+ }
+
+ Assert(i == nFreeKeys);
+ /* sort the pathkeys by sort cost in ascending order */
+ qsort(costs, nFreeKeys, sizeof(*costs), pathkey_sort_cost_comparator);
+
+ /*
+ * Rebuild the list of pathkeys - first the preordered ones, then the
+ * rest ordered by cost.
+ */
+ new_group_pathkeys = list_copy_head(*group_pathkeys, n_preordered);
+
+ for (int i = 0; i < nFreeKeys; i++)
+ new_group_pathkeys = lappend(new_group_pathkeys, costs[i].pathkey);
+
+ new_group_pathkeys = list_copy_tail(*group_pathkeys,
+ root->num_groupby_pathkeys);
+ pfree(costs);
+ }
+ else
+ {
+ /* Copy the list, so that we can free the new list by list_free. */
+ new_group_pathkeys = list_copy(*group_pathkeys);
+ nToPermute = nFreeKeys;
+ }
+
+ Assert(list_length(new_group_pathkeys) == list_length(*group_pathkeys));
+
+ /*
+ * Generate pathkey lists with permutations of the first nToPermute
+ * pathkeys.
+ *
+ * XXX We simply calculate sort cost for each individual pathkey list, but
+ * there's room for two dynamic programming optimizations here. Firstly,
+ * we may pass the current "best" cost to cost_sort_estimate so that it
+ * can "abort" if the estimated pathkeys list exceeds it. Secondly, it
+ * could pass the return information about the position when it exceeded
+ * the cost, and we could skip all permutations with the same prefix.
+ *
+ * Imagine we've already found ordering with cost C1, and we're evaluating
+ * another ordering - cost_sort_estimate() calculates cost by adding the
+ * pathkeys one by one (more or less), and the cost only grows. If at any
+ * point it exceeds C1, it can't possibly be "better" so we can discard
+ * it. But we also know that we can discard all ordering with the same
+ * prefix, because if we're estimating (a,b,c,d) and we exceed C1 at (a,b)
+ * then the same thing will happen for any ordering with this prefix.
+ */
+ PathkeyMutatorInit(&mstate, new_group_pathkeys, n_preordered,
+ n_preordered + nToPermute);
+
+ while ((var_group_pathkeys = PathkeyMutatorNext(&mstate)) != NIL)
+ {
+ Cost cost;
+
+ cost = cost_sort_estimate(root, var_group_pathkeys, n_preordered, nrows);
+
+ if (cost < cheapest_sort_cost)
+ {
+ list_free(new_group_pathkeys);
+ new_group_pathkeys = list_copy(var_group_pathkeys);
+ cheapest_sort_cost = cost;
+ }
+ }
+
+ Assert(list_length(new_group_pathkeys) == list_length(root->group_pathkeys));
+
+ /* Reorder the group clauses according to the reordered pathkeys. */
+ foreach(cell, new_group_pathkeys)
+ {
+ PathKey *pathkey = (PathKey *) lfirst(cell);
+
+ if (foreach_current_index(cell) >= root->num_groupby_pathkeys)
+ break;
+
+ new_group_clauses =
+ lappend(new_group_clauses,
+ get_sortgroupref_clause(pathkey->pk_eclass->ec_sortref,
+ *group_clauses));
+ }
+
+ /* Just append the rest GROUP BY clauses */
+ new_group_clauses = list_concat_unique_ptr(new_group_clauses,
+ *group_clauses);
+ *group_pathkeys = new_group_pathkeys;
+ *group_clauses = new_group_clauses;
+ Assert(list_length(new_group_clauses) == list_length(root->processed_groupClause));
+
+ return true;
+}
+
+/*
+ * get_useful_group_keys_orderings
+ * Determine which orderings of GROUP BY keys are potentially interesting.
+ *
+ * Returns list of PathKeyInfo items, each representing an interesting ordering
+ * of GROUP BY keys. Each item stores pathkeys and clauses in matching order.
+ *
+ * The function considers (and keeps) multiple group by orderings:
+ *
+ * - the original ordering, as specified by the GROUP BY clause
+ *
+ * - GROUP BY keys reordered to minimize the sort cost
+ *
+ * - GROUP BY keys reordered to match path ordering (as much as possible), with
+ * the tail reordered to minimize the sort cost
+ *
+ * - GROUP BY keys to match target ORDER BY clause (as much as possible), with
+ * the tail reordered to minimize the sort cost
+ *
+ * There are other potentially interesting orderings (e.g. it might be best to
+ * match the first ORDER BY key, order the remaining keys differently and then
+ * rely on the incremental sort to fix this), but we ignore those for now. To
+ * make this work we'd have to pretty much generate all possible permutations.
+ */
+List *
+get_useful_group_keys_orderings(PlannerInfo *root, double nrows,
+ List *path_pathkeys)
+{
+ Query *parse = root->parse;
+ List *infos = NIL;
+ PathKeyInfo *info;
+ int n_preordered = 0;
+
+ List *pathkeys = root->group_pathkeys;
+ List *clauses = root->processed_groupClause;
+
+ /* always return at least the original pathkeys/clauses */
+ info = makeNode(PathKeyInfo);
+ info->pathkeys = pathkeys;
+ info->clauses = clauses;
+ infos = lappend(infos, info);
+
+ /*
+ * Should we try generating alternative orderings of the group keys? If
+ * not, we produce only the order specified in the query, i.e. the
+ * optimization is effectively disabled.
+ */
+ if (!enable_group_by_reordering)
+ return infos;
+
+ /* for grouping sets we can't do any reordering */
+ if (parse->groupingSets)
+ return infos;
+
+ /*
+ * Try reordering pathkeys to minimize the sort cost, ignoring both the
+ * target ordering (ORDER BY) and ordering of the input path.
+ */
+ if (get_cheapest_group_keys_order(root, nrows, &pathkeys, &clauses,
+ n_preordered))
+ {
+ Assert(list_length(pathkeys) == list_length(clauses));
+
+ info = makeNode(PathKeyInfo);
+ info->pathkeys = pathkeys;
+ info->clauses = clauses;
+
+ infos = lappend(infos, info);
+ }
+
+ /*
+ * If the path is sorted in some way, try reordering the group keys to
+ * match as much of the ordering as possible - we get this sort for free
+ * (mostly).
+ *
+ * We must not do this when there are no grouping sets, because those use
+ * more complex logic to decide the ordering.
+ *
+ * XXX Isn't this somewhat redundant with presorted_keys? Actually, it's
+ * more a complement, because it allows benefiting from incremental sort
+ * as much as possible.
+ *
+ * XXX This does nothing if (n_preordered == 0). We shouldn't create the
+ * info in this case.
+ */
+ if (path_pathkeys)
+ {
+ n_preordered = group_keys_reorder_by_pathkeys(path_pathkeys,
+ &pathkeys,
+ &clauses,
+ root->num_groupby_pathkeys);
+
+ /* reorder the tail to minimize sort cost */
+ get_cheapest_group_keys_order(root, nrows, &pathkeys, &clauses,
+ n_preordered);
+
+ /*
+ * reorder the tail to minimize sort cost
+ *
+ * XXX Ignore the return value - there may be nothing to reorder, in
+ * which case get_cheapest_group_keys_order returns false. But we
+ * still want to keep the keys reordered to path_pathkeys.
+ */
+ info = makeNode(PathKeyInfo);
+ info->pathkeys = pathkeys;
+ info->clauses = clauses;
+
+ infos = lappend(infos, info);
+ }
+
+ /*
+ * Try reordering pathkeys to minimize the sort cost (this time consider
+ * the ORDER BY clause, but only if set debug_group_by_match_order_by).
+ */
+ if (root->sort_pathkeys)
+ {
+ n_preordered = group_keys_reorder_by_pathkeys(root->sort_pathkeys,
+ &pathkeys,
+ &clauses,
+ root->num_groupby_pathkeys);
+
+ /*
+ * reorder the tail to minimize sort cost
+ *
+ * XXX Ignore the return value - there may be nothing to reorder, in
+ * which case get_cheapest_group_keys_order returns false. But we
+ * still want to keep the keys reordered to sort_pathkeys.
+ */
+ get_cheapest_group_keys_order(root, nrows, &pathkeys, &clauses,
+ n_preordered);
+
+ /* keep the group keys reordered to match ordering of input path */
+ info = makeNode(PathKeyInfo);
+ info->pathkeys = pathkeys;
+ info->clauses = clauses;
+
+ infos = lappend(infos, info);
+ }
+
+ return infos;
+}
+
/*
* pathkeys_count_contained_in
* Same as pathkeys_contained_in, but also sets length of longest
@@ -1939,6 +2488,54 @@ pathkeys_useful_for_ordering(PlannerInfo *root, List *pathkeys)
return n_common_pathkeys;
}
+/*
+ * pathkeys_useful_for_grouping
+ * Count the number of pathkeys that are useful for grouping (instead of
+ * explicit sort)
+ *
+ * Group pathkeys could be reordered to benefit from the ordering. The
+ * ordering may not be "complete" and may require incremental sort, but that's
+ * fine. So we simply count prefix pathkeys with a matching group key, and
+ * stop once we find the first pathkey without a match.
+ *
+ * So e.g. with pathkeys (a,b,c) and group keys (a,b,e) this determines (a,b)
+ * pathkeys are useful for grouping, and we might do incremental sort to get
+ * path ordered by (a,b,e).
+ *
+ * This logic is necessary to retain paths with ordering not matching grouping
+ * keys directly, without the reordering.
+ *
+ * Returns the length of pathkey prefix with matching group keys.
+ */
+static int
+pathkeys_useful_for_grouping(PlannerInfo *root, List *pathkeys)
+{
+ ListCell *key;
+ int n = 0;
+
+ /* no special ordering requested for grouping */
+ if (root->group_pathkeys == NIL)
+ return 0;
+
+ /* unordered path */
+ if (pathkeys == NIL)
+ return 0;
+
+ /* walk the pathkeys and search for matching group key */
+ foreach(key, pathkeys)
+ {
+ PathKey *pathkey = (PathKey *) lfirst(key);
+
+ /* no matching group key, we're done */
+ if (!list_member_ptr(root->group_pathkeys, pathkey))
+ break;
+
+ n++;
+ }
+
+ return n;
+}
+
/*
* truncate_useless_pathkeys
* Shorten the given pathkey list to just the useful pathkeys.
@@ -1953,6 +2550,9 @@ truncate_useless_pathkeys(PlannerInfo *root,
nuseful = pathkeys_useful_for_merging(root, rel, pathkeys);
nuseful2 = pathkeys_useful_for_ordering(root, pathkeys);
+ if (nuseful2 > nuseful)
+ nuseful = nuseful2;
+ nuseful2 = pathkeys_useful_for_grouping(root, pathkeys);
if (nuseful2 > nuseful)
nuseful = nuseful2;
@@ -1988,6 +2588,8 @@ has_useful_pathkeys(PlannerInfo *root, RelOptInfo *rel)
{
if (rel->joininfo != NIL || rel->has_eclass_joins)
return true; /* might be able to use pathkeys for merging */
+ if (root->group_pathkeys != NIL)
+ return true; /* might be able to use pathkeys for grouping */
if (root->query_pathkeys != NIL)
return true; /* might be able to use them for ordering */
return false; /* definitely useless */
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 44efb1f4eb..4a7c9d9440 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -2810,8 +2810,9 @@ remove_useless_groupby_columns(PlannerInfo *root)
*
* In principle it might be interesting to consider other orderings of the
* GROUP BY elements, which could match the sort ordering of other
- * possible plans (eg an indexscan) and thereby reduce cost. We don't
- * bother with that, though. Hashed grouping will frequently win anyway.
+ * possible plans (eg an indexscan) and thereby reduce cost. However, we
+ * don't yet have sufficient information to do that here, so that's left until
+ * later in planning. See get_useful_group_keys_orderings().
*
* Note: we need no comparable processing of the distinctClause because
* the parser already enforced that that matches ORDER BY.
@@ -3468,6 +3469,7 @@ standard_qp_callback(PlannerInfo *root, void *extra)
tlist,
true,
&sortable);
+
if (!sortable)
{
/* Can't sort; no point in considering aggregate ordering either */
@@ -6842,90 +6844,108 @@ add_paths_to_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel,
*/
foreach(lc, input_rel->pathlist)
{
+ ListCell *lc2;
Path *path = (Path *) lfirst(lc);
- bool is_sorted;
- int presorted_keys;
+ Path *path_save = path;
+ List *pathkey_orderings = NIL;
- is_sorted = pathkeys_count_contained_in(root->group_pathkeys,
- path->pathkeys,
- &presorted_keys);
+ /* generate alternative group orderings that might be useful */
+ pathkey_orderings = get_useful_group_keys_orderings(root,
+ path->rows,
+ path->pathkeys);
+
+ Assert(list_length(pathkey_orderings) > 0);
- if (!is_sorted)
+ foreach(lc2, pathkey_orderings)
{
- /*
- * Try at least sorting the cheapest path and also try
- * incrementally sorting any path which is partially sorted
- * already (no need to deal with paths which have presorted
- * keys when incremental sort is disabled unless it's the
- * cheapest input path).
- */
- if (path != cheapest_path &&
- (presorted_keys == 0 || !enable_incremental_sort))
- continue;
+ bool is_sorted;
+ int presorted_keys;
+ PathKeyInfo *info = (PathKeyInfo *) lfirst(lc2);
- /*
- * We've no need to consider both a sort and incremental sort.
- * We'll just do a sort if there are no presorted keys and an
- * incremental sort when there are presorted keys.
- */
- if (presorted_keys == 0 || !enable_incremental_sort)
- path = (Path *) create_sort_path(root,
- grouped_rel,
- path,
- root->group_pathkeys,
- -1.0);
- else
- path = (Path *) create_incremental_sort_path(root,
- grouped_rel,
- path,
- root->group_pathkeys,
- presorted_keys,
- -1.0);
- }
+ /* restore the path (we replace it in the loop) */
+ path = path_save;
- /* Now decide what to stick atop it */
- if (parse->groupingSets)
- {
- consider_groupingsets_paths(root, grouped_rel,
- path, true, can_hash,
- gd, agg_costs, dNumGroups);
- }
- else if (parse->hasAggs)
- {
- /*
- * We have aggregation, possibly with plain GROUP BY. Make an
- * AggPath.
- */
- add_path(grouped_rel, (Path *)
+ is_sorted = pathkeys_count_contained_in(info->pathkeys,
+ path->pathkeys,
+ &presorted_keys);
+
+ if (!is_sorted)
+ {
+ /*
+ * Try at least sorting the cheapest path and also try
+ * incrementally sorting any path which is partially sorted
+ * already (no need to deal with paths which have presorted
+ * keys when incremental sort is disabled unless it's the
+ * cheapest input path).
+ */
+ if (path != cheapest_path &&
+ (presorted_keys == 0 || !enable_incremental_sort))
+ continue;
+
+ /*
+ * We've no need to consider both a sort and incremental sort.
+ * We'll just do a sort if there are no presorted keys and an
+ * incremental sort when there are presorted keys.
+ */
+ if (presorted_keys == 0 || !enable_incremental_sort)
+ path = (Path *) create_sort_path(root,
+ grouped_rel,
+ path,
+ info->pathkeys,
+ -1.0);
+ else
+ path = (Path *) create_incremental_sort_path(root,
+ grouped_rel,
+ path,
+ info->pathkeys,
+ presorted_keys,
+ -1.0);
+ }
+
+ /* Now decide what to stick atop it */
+ if (parse->groupingSets)
+ {
+ consider_groupingsets_paths(root, grouped_rel,
+ path, true, can_hash,
+ gd, agg_costs, dNumGroups);
+ }
+ else if (parse->hasAggs)
+ {
+ /*
+ * We have aggregation, possibly with plain GROUP BY. Make an
+ * AggPath.
+ */
+ add_path(grouped_rel, (Path *)
create_agg_path(root,
grouped_rel,
path,
grouped_rel->reltarget,
parse->groupClause ? AGG_SORTED : AGG_PLAIN,
AGGSPLIT_SIMPLE,
- root->processed_groupClause,
+ info->clauses,
havingQual,
agg_costs,
dNumGroups));
- }
- else if (parse->groupClause)
- {
- /*
- * We have GROUP BY without aggregation or grouping sets. Make
- * a GroupPath.
- */
- add_path(grouped_rel, (Path *)
+ }
+ else if (parse->groupClause)
+ {
+ /*
+ * We have GROUP BY without aggregation or grouping sets. Make
+ * a GroupPath.
+ */
+ add_path(grouped_rel, (Path *)
create_group_path(root,
grouped_rel,
path,
- root->processed_groupClause,
+ info->clauses,
havingQual,
dNumGroups));
- }
- else
- {
- /* Other cases should have been handled above */
- Assert(false);
+ }
+ else
+ {
+ /* Other cases should have been handled above */
+ Assert(false);
+ }
}
}
@@ -6937,69 +6957,84 @@ add_paths_to_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel,
{
foreach(lc, partially_grouped_rel->pathlist)
{
+ ListCell *lc2;
Path *path = (Path *) lfirst(lc);
- bool is_sorted;
- int presorted_keys;
+ List *pathkey_orderings = NIL;
- is_sorted = pathkeys_count_contained_in(root->group_pathkeys,
- path->pathkeys,
- &presorted_keys);
+ /* generate alternative group orderings that might be useful */
+ pathkey_orderings = get_useful_group_keys_orderings(root,
+ path->rows,
+ path->pathkeys);
- if (!is_sorted)
+ Assert(list_length(pathkey_orderings) > 0);
+
+ /* process all potentially interesting grouping reorderings */
+ foreach(lc2, pathkey_orderings)
{
- /*
- * Try at least sorting the cheapest path and also try
- * incrementally sorting any path which is partially
- * sorted already (no need to deal with paths which have
- * presorted keys when incremental sort is disabled unless
- * it's the cheapest input path).
- */
- if (path != partially_grouped_rel->cheapest_total_path &&
- (presorted_keys == 0 || !enable_incremental_sort))
- continue;
+ bool is_sorted;
+ int presorted_keys;
+ PathKeyInfo *info = (PathKeyInfo *) lfirst(lc2);
- /*
- * We've no need to consider both a sort and incremental
- * sort. We'll just do a sort if there are no pre-sorted
- * keys and an incremental sort when there are presorted
- * keys.
- */
- if (presorted_keys == 0 || !enable_incremental_sort)
- path = (Path *) create_sort_path(root,
- grouped_rel,
- path,
- root->group_pathkeys,
- -1.0);
- else
- path = (Path *) create_incremental_sort_path(root,
- grouped_rel,
- path,
- root->group_pathkeys,
- presorted_keys,
- -1.0);
- }
+ is_sorted = pathkeys_count_contained_in(root->group_pathkeys,
+ path->pathkeys,
+ &presorted_keys);
- if (parse->hasAggs)
- add_path(grouped_rel, (Path *)
+ if (!is_sorted)
+ {
+ /*
+ * Try at least sorting the cheapest path and also try
+ * incrementally sorting any path which is partially
+ * sorted already (no need to deal with paths which have
+ * presorted keys when incremental sort is disabled unless
+ * it's the cheapest input path).
+ */
+ if (path != partially_grouped_rel->cheapest_total_path &&
+ (presorted_keys == 0 || !enable_incremental_sort))
+ continue;
+
+ /*
+ * We've no need to consider both a sort and incremental
+ * sort. We'll just do a sort if there are no pre-sorted
+ * keys and an incremental sort when there are presorted
+ * keys.
+ */
+ if (presorted_keys == 0 || !enable_incremental_sort)
+ path = (Path *) create_sort_path(root,
+ grouped_rel,
+ path,
+ info->pathkeys,
+ -1.0);
+ else
+ path = (Path *) create_incremental_sort_path(root,
+ grouped_rel,
+ path,
+ info->pathkeys,
+ presorted_keys,
+ -1.0);
+ }
+
+ if (parse->hasAggs)
+ add_path(grouped_rel, (Path *)
create_agg_path(root,
grouped_rel,
path,
grouped_rel->reltarget,
parse->groupClause ? AGG_SORTED : AGG_PLAIN,
AGGSPLIT_FINAL_DESERIAL,
- root->processed_groupClause,
+ info->clauses,
havingQual,
agg_final_costs,
dNumGroups));
- else
- add_path(grouped_rel, (Path *)
+ else
+ add_path(grouped_rel, (Path *)
create_group_path(root,
grouped_rel,
path,
- root->processed_groupClause,
+ info->clauses,
havingQual,
dNumGroups));
+ }
}
}
}
@@ -7202,66 +7237,85 @@ create_partial_grouping_paths(PlannerInfo *root,
*/
foreach(lc, input_rel->pathlist)
{
+ ListCell *lc2;
Path *path = (Path *) lfirst(lc);
- bool is_sorted;
- int presorted_keys;
+ Path *path_save = path;
+ List *pathkey_orderings = NIL;
- is_sorted = pathkeys_count_contained_in(root->group_pathkeys,
- path->pathkeys,
- &presorted_keys);
- if (!is_sorted)
+ /* generate alternative group orderings that might be useful */
+ pathkey_orderings = get_useful_group_keys_orderings(root,
+ path->rows,
+ path->pathkeys);
+
+ Assert(list_length(pathkey_orderings) > 0);
+
+ /* process all potentially interesting grouping reorderings */
+ foreach(lc2, pathkey_orderings)
{
- /*
- * Try at least sorting the cheapest path and also try
- * incrementally sorting any path which is partially sorted
- * already (no need to deal with paths which have presorted
- * keys when incremental sort is disabled unless it's the
- * cheapest input path).
- */
- if (path != cheapest_total_path &&
- (presorted_keys == 0 || !enable_incremental_sort))
- continue;
+ bool is_sorted;
+ int presorted_keys;
+ PathKeyInfo *info = (PathKeyInfo *) lfirst(lc2);
- /*
- * We've no need to consider both a sort and incremental sort.
- * We'll just do a sort if there are no presorted keys and an
- * incremental sort when there are presorted keys.
- */
- if (presorted_keys == 0 || !enable_incremental_sort)
- path = (Path *) create_sort_path(root,
- partially_grouped_rel,
- path,
- root->group_pathkeys,
- -1.0);
- else
- path = (Path *) create_incremental_sort_path(root,
- partially_grouped_rel,
- path,
- root->group_pathkeys,
- presorted_keys,
- -1.0);
- }
+ /* restore the path (we replace it in the loop) */
+ path = path_save;
- if (parse->hasAggs)
- add_path(partially_grouped_rel, (Path *)
+ is_sorted = pathkeys_count_contained_in(info->pathkeys,
+ path->pathkeys,
+ &presorted_keys);
+ if (!is_sorted)
+ {
+ /*
+ * Try at least sorting the cheapest path and also try
+ * incrementally sorting any path which is partially sorted
+ * already (no need to deal with paths which have presorted
+ * keys when incremental sort is disabled unless it's the
+ * cheapest input path).
+ */
+ if (path != cheapest_total_path &&
+ (presorted_keys == 0 || !enable_incremental_sort))
+ continue;
+
+ /*
+ * We've no need to consider both a sort and incremental sort.
+ * We'll just do a sort if there are no presorted keys and an
+ * incremental sort when there are presorted keys.
+ */
+ if (presorted_keys == 0 || !enable_incremental_sort)
+ path = (Path *) create_sort_path(root,
+ partially_grouped_rel,
+ path,
+ info->pathkeys,
+ -1.0);
+ else
+ path = (Path *) create_incremental_sort_path(root,
+ partially_grouped_rel,
+ path,
+ info->pathkeys,
+ presorted_keys,
+ -1.0);
+ }
+
+ if (parse->hasAggs)
+ add_path(partially_grouped_rel, (Path *)
create_agg_path(root,
partially_grouped_rel,
path,
partially_grouped_rel->reltarget,
parse->groupClause ? AGG_SORTED : AGG_PLAIN,
AGGSPLIT_INITIAL_SERIAL,
- root->processed_groupClause,
+ info->clauses,
NIL,
agg_partial_costs,
dNumPartialGroups));
- else
- add_path(partially_grouped_rel, (Path *)
- create_group_path(root,
- partially_grouped_rel,
- path,
- root->processed_groupClause,
- NIL,
- dNumPartialGroups));
+ else
+ add_path(partially_grouped_rel, (Path *)
+ create_group_path(root,
+ partially_grouped_rel,
+ path,
+ info->clauses,
+ NIL,
+ dNumPartialGroups));
+ }
}
}
@@ -7270,67 +7324,86 @@ create_partial_grouping_paths(PlannerInfo *root,
/* Similar to above logic, but for partial paths. */
foreach(lc, input_rel->partial_pathlist)
{
+ ListCell *lc2;
Path *path = (Path *) lfirst(lc);
- bool is_sorted;
- int presorted_keys;
+ Path *path_save = path;
+ List *pathkey_orderings = NIL;
- is_sorted = pathkeys_count_contained_in(root->group_pathkeys,
- path->pathkeys,
- &presorted_keys);
+ /* generate alternative group orderings that might be useful */
+ pathkey_orderings = get_useful_group_keys_orderings(root,
+ path->rows,
+ path->pathkeys);
+
+ Assert(list_length(pathkey_orderings) > 0);
- if (!is_sorted)
+ /* process all potentially interesting grouping reorderings */
+ foreach(lc2, pathkey_orderings)
{
- /*
- * Try at least sorting the cheapest path and also try
- * incrementally sorting any path which is partially sorted
- * already (no need to deal with paths which have presorted
- * keys when incremental sort is disabled unless it's the
- * cheapest input path).
- */
- if (path != cheapest_partial_path &&
- (presorted_keys == 0 || !enable_incremental_sort))
- continue;
+ bool is_sorted;
+ int presorted_keys;
+ PathKeyInfo *info = (PathKeyInfo *) lfirst(lc2);
- /*
- * We've no need to consider both a sort and incremental sort.
- * We'll just do a sort if there are no presorted keys and an
- * incremental sort when there are presorted keys.
- */
- if (presorted_keys == 0 || !enable_incremental_sort)
- path = (Path *) create_sort_path(root,
- partially_grouped_rel,
- path,
- root->group_pathkeys,
- -1.0);
- else
- path = (Path *) create_incremental_sort_path(root,
- partially_grouped_rel,
- path,
- root->group_pathkeys,
- presorted_keys,
- -1.0);
- }
+ /* restore the path (we replace it in the loop) */
+ path = path_save;
- if (parse->hasAggs)
- add_partial_path(partially_grouped_rel, (Path *)
+ is_sorted = pathkeys_count_contained_in(info->pathkeys,
+ path->pathkeys,
+ &presorted_keys);
+
+ if (!is_sorted)
+ {
+ /*
+ * Try at least sorting the cheapest path and also try
+ * incrementally sorting any path which is partially sorted
+ * already (no need to deal with paths which have presorted
+ * keys when incremental sort is disabled unless it's the
+ * cheapest input path).
+ */
+ if (path != cheapest_partial_path &&
+ (presorted_keys == 0 || !enable_incremental_sort))
+ continue;
+
+ /*
+ * We've no need to consider both a sort and incremental sort.
+ * We'll just do a sort if there are no presorted keys and an
+ * incremental sort when there are presorted keys.
+ */
+ if (presorted_keys == 0 || !enable_incremental_sort)
+ path = (Path *) create_sort_path(root,
+ partially_grouped_rel,
+ path,
+ info->pathkeys,
+ -1.0);
+ else
+ path = (Path *) create_incremental_sort_path(root,
+ partially_grouped_rel,
+ path,
+ info->pathkeys,
+ presorted_keys,
+ -1.0);
+ }
+
+ if (parse->hasAggs)
+ add_partial_path(partially_grouped_rel, (Path *)
create_agg_path(root,
partially_grouped_rel,
path,
partially_grouped_rel->reltarget,
parse->groupClause ? AGG_SORTED : AGG_PLAIN,
AGGSPLIT_INITIAL_SERIAL,
- root->processed_groupClause,
+ info->clauses,
NIL,
agg_partial_costs,
dNumPartialPartialGroups));
- else
- add_partial_path(partially_grouped_rel, (Path *)
+ else
+ add_partial_path(partially_grouped_rel, (Path *)
create_group_path(root,
partially_grouped_rel,
path,
- root->processed_groupClause,
+ info->clauses,
NIL,
dNumPartialPartialGroups));
+ }
}
}
@@ -7444,6 +7517,8 @@ gather_grouping_paths(PlannerInfo *root, RelOptInfo *rel)
* We can also skip the entire loop when we only have a single-item
* group_pathkeys because then we can't possibly have a presorted prefix
* of the list without having the list be fully sorted.
+ *
+ * XXX Shouldn't this also consider the group-key-reordering?
*/
if (!enable_incremental_sort || list_length(root->group_pathkeys) == 1)
return;
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 211ba65389..7b8d0cd294 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1346,12 +1346,12 @@ create_append_path(PlannerInfo *root,
pathnode->path.total_cost = child->total_cost;
}
else
- cost_append(pathnode);
+ cost_append(pathnode, root);
/* Must do this last, else cost_append complains */
pathnode->path.pathkeys = child->pathkeys;
}
else
- cost_append(pathnode);
+ cost_append(pathnode, root);
/* If the caller provided a row estimate, override the computed value. */
if (rows >= 0)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index c4fcd0076e..cb7e67099e 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -3386,11 +3386,28 @@ double
estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
List **pgset, EstimationInfo *estinfo)
{
- List *varinfos = NIL;
+ return estimate_num_groups_incremental(root, groupExprs,
+ input_rows, pgset, estinfo,
+ NULL, 0);
+}
+
+/*
+ * estimate_num_groups_incremental
+ * An estimate_num_groups variant, optimized for cases that are adding the
+ * expressions incrementally (e.g. one by one).
+ */
+double
+estimate_num_groups_incremental(PlannerInfo *root, List *groupExprs,
+ double input_rows,
+ List **pgset, EstimationInfo *estinfo,
+ List **cache_varinfos, int prevNExprs)
+{
+ List *varinfos = (cache_varinfos) ? *cache_varinfos : NIL;
double srf_multiplier = 1.0;
double numdistinct;
ListCell *l;
- int i;
+ int i,
+ j;
/* Zero the estinfo output parameter, if non-NULL */
if (estinfo != NULL)
@@ -3421,7 +3438,7 @@ estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
*/
numdistinct = 1.0;
- i = 0;
+ i = j = 0;
foreach(l, groupExprs)
{
Node *groupexpr = (Node *) lfirst(l);
@@ -3430,6 +3447,14 @@ estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
List *varshere;
ListCell *l2;
+ /* was done on previous call */
+ if (cache_varinfos && j++ < prevNExprs)
+ {
+ if (pgset)
+ i++; /* to keep in sync with lines below */
+ continue;
+ }
+
/* is expression in this grouping set? */
if (pgset && !list_member_int(*pgset, i++))
continue;
@@ -3499,7 +3524,11 @@ estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
if (varshere == NIL)
{
if (contain_volatile_functions(groupexpr))
+ {
+ if (cache_varinfos)
+ *cache_varinfos = varinfos;
return input_rows;
+ }
continue;
}
@@ -3516,6 +3545,9 @@ estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
}
}
+ if (cache_varinfos)
+ *cache_varinfos = varinfos;
+
/*
* If now no Vars, we must have an all-constant or all-boolean GROUP BY
* list.
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index bdb26e2b77..385f160ff8 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -1026,6 +1026,16 @@ struct config_bool ConfigureNamesBool[] =
true,
NULL, NULL, NULL
},
+ {
+ {"enable_group_by_reordering", PGC_USERSET, QUERY_TUNING_METHOD,
+ gettext_noop("Enables reordering of GROUP BY keys."),
+ NULL,
+ GUC_EXPLAIN
+ },
+ &enable_group_by_reordering,
+ true,
+ NULL, NULL, NULL
+ },
{
{"geqo", PGC_USERSET, QUERY_TUNING_GEQO,
gettext_noop("Enables genetic query optimization."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 6bb39d39ba..751511745a 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -391,6 +391,7 @@
#enable_seqscan = on
#enable_sort = on
#enable_tidscan = on
+#enable_group_by_reordering = on
# - Planner Cost Constants -
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 5702fbba60..15ba2f8218 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -1456,6 +1456,16 @@ typedef struct PathKey
bool pk_nulls_first; /* do NULLs come before normal values? */
} PathKey;
+/*
+ * Combines information about pathkeys and the associated clauses.
+ */
+typedef struct PathKeyInfo
+{
+ NodeTag type;
+ List *pathkeys;
+ List *clauses;
+} PathKeyInfo;
+
/*
* VolatileFunctionStatus -- allows nodes to cache their
* contain_volatile_functions properties. VOLATILITY_UNKNOWN means not yet
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index bee090ffc2..bbb1132473 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -116,7 +116,9 @@ extern void cost_incremental_sort(Path *path,
Cost input_startup_cost, Cost input_total_cost,
double input_tuples, int width, Cost comparison_cost, int sort_mem,
double limit_tuples);
-extern void cost_append(AppendPath *apath);
+extern Cost cost_sort_estimate(PlannerInfo *root, List *pathkeys,
+ int nPresortedKeys, double tuples);
+extern void cost_append(AppendPath *path, PlannerInfo *root);
extern void cost_merge_append(Path *path, PlannerInfo *root,
List *pathkeys, int n_streams,
Cost input_startup_cost, Cost input_total_cost,
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index 50bc3b503a..e4ff16d0c4 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -24,6 +24,7 @@ extern PGDLLIMPORT bool enable_geqo;
extern PGDLLIMPORT int geqo_threshold;
extern PGDLLIMPORT int min_parallel_table_scan_size;
extern PGDLLIMPORT int min_parallel_index_scan_size;
+extern PGDLLIMPORT bool enable_group_by_reordering;
/* Hook for plugins to get control in set_rel_pathlist() */
typedef void (*set_rel_pathlist_hook_type) (PlannerInfo *root,
@@ -205,6 +206,12 @@ typedef enum
extern PathKeysComparison compare_pathkeys(List *keys1, List *keys2);
extern bool pathkeys_contained_in(List *keys1, List *keys2);
extern bool pathkeys_count_contained_in(List *keys1, List *keys2, int *n_common);
+extern int group_keys_reorder_by_pathkeys(List *pathkeys,
+ List **group_pathkeys,
+ List **group_clauses,
+ int num_groupby_pathkeys);
+extern List *get_useful_group_keys_orderings(PlannerInfo *root, double nrows,
+ List *path_pathkeys);
extern Path *get_cheapest_path_for_pathkeys(List *paths, List *pathkeys,
Relids required_outer,
CostSelector cost_criterion,
diff --git a/src/include/utils/selfuncs.h b/src/include/utils/selfuncs.h
index 2f76c473db..2b21c06cc9 100644
--- a/src/include/utils/selfuncs.h
+++ b/src/include/utils/selfuncs.h
@@ -214,6 +214,11 @@ extern double estimate_num_groups(PlannerInfo *root, List *groupExprs,
double input_rows, List **pgset,
EstimationInfo *estinfo);
+extern double estimate_num_groups_incremental(PlannerInfo *root, List *groupExprs,
+ double input_rows, List **pgset,
+ EstimationInfo *estinfo,
+ List **cache_varinfos, int prevNExprs);
+
extern void estimate_hash_bucket_stats(PlannerInfo *root,
Node *hashkey, double nbuckets,
Selectivity *mcv_freq,
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index d8271da4d1..205be28b3d 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -1234,7 +1234,8 @@ explain (costs off)
select distinct min(f1), max(f1) from minmaxtest;
QUERY PLAN
---------------------------------------------------------------------------------------------
- Unique
+ HashAggregate
+ Group Key: $0, $1
InitPlan 1 (returns $0)
-> Limit
-> Merge Append
@@ -1257,10 +1258,8 @@ explain (costs off)
-> Index Only Scan using minmaxtest2i on minmaxtest2 minmaxtest_8
Index Cond: (f1 IS NOT NULL)
-> Index Only Scan Backward using minmaxtest3i on minmaxtest3 minmaxtest_9
- -> Sort
- Sort Key: ($0), ($1)
- -> Result
-(26 rows)
+ -> Result
+(25 rows)
select distinct min(f1), max(f1) from minmaxtest;
min | max
@@ -2715,6 +2714,241 @@ SELECT balk(hundred) FROM tenk1;
(1 row)
ROLLBACK;
+-- GROUP BY optimization by reorder columns
+SELECT
+ i AS id,
+ i/2 AS p,
+ format('%60s', i%2) AS v,
+ i/4 AS c,
+ i/8 AS d,
+ (random() * (10000/8))::int as e --the same as d but no correlation with p
+ INTO btg
+FROM
+ generate_series(1, 10000) i;
+VACUUM btg;
+ANALYZE btg;
+-- GROUP BY optimization by reorder columns by frequency
+SET enable_hashagg=off;
+SET max_parallel_workers= 0;
+SET max_parallel_workers_per_gather = 0;
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, v;
+ QUERY PLAN
+-----------------------------
+ GroupAggregate
+ Group Key: p, v
+ -> Sort
+ Sort Key: p, v
+ -> Seq Scan on btg
+(5 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p;
+ QUERY PLAN
+-----------------------------
+ GroupAggregate
+ Group Key: p, v
+ -> Sort
+ Sort Key: p, v
+ -> Seq Scan on btg
+(5 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, c;
+ QUERY PLAN
+-----------------------------
+ GroupAggregate
+ Group Key: p, c, v
+ -> Sort
+ Sort Key: p, c, v
+ -> Seq Scan on btg
+(5 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, c ORDER BY v, p, c;
+ QUERY PLAN
+-----------------------------
+ GroupAggregate
+ Group Key: v, p, c
+ -> Sort
+ Sort Key: v, p, c
+ -> Seq Scan on btg
+(5 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, d, c;
+ QUERY PLAN
+------------------------------
+ GroupAggregate
+ Group Key: p, d, v, c
+ -> Sort
+ Sort Key: p, d, v, c
+ -> Seq Scan on btg
+(5 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, d, c ORDER BY v, p, d ,c;
+ QUERY PLAN
+------------------------------
+ GroupAggregate
+ Group Key: v, p, d, c
+ -> Sort
+ Sort Key: v, p, d, c
+ -> Seq Scan on btg
+(5 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, d, c ORDER BY p, v, d ,c;
+ QUERY PLAN
+------------------------------
+ GroupAggregate
+ Group Key: p, v, d, c
+ -> Sort
+ Sort Key: p, v, d, c
+ -> Seq Scan on btg
+(5 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, d, e;
+ QUERY PLAN
+-----------------------------
+ GroupAggregate
+ Group Key: p, d, e
+ -> Sort
+ Sort Key: p, d, e
+ -> Seq Scan on btg
+(5 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, e, d;
+ QUERY PLAN
+-----------------------------
+ GroupAggregate
+ Group Key: p, e, d
+ -> Sort
+ Sort Key: p, e, d
+ -> Seq Scan on btg
+(5 rows)
+
+CREATE STATISTICS btg_dep ON d, e, p FROM btg;
+ANALYZE btg;
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, d, e;
+ QUERY PLAN
+-----------------------------
+ GroupAggregate
+ Group Key: p, d, e
+ -> Sort
+ Sort Key: p, d, e
+ -> Seq Scan on btg
+(5 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, e, d;
+ QUERY PLAN
+-----------------------------
+ GroupAggregate
+ Group Key: p, e, d
+ -> Sort
+ Sort Key: p, e, d
+ -> Seq Scan on btg
+(5 rows)
+
+-- GROUP BY optimization by reorder columns by index scan
+CREATE INDEX ON btg(p, v);
+SET enable_seqscan=off;
+SET enable_bitmapscan=off;
+VACUUM btg;
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, v;
+ QUERY PLAN
+------------------------------------------------
+ GroupAggregate
+ Group Key: p, v
+ -> Index Only Scan using btg_p_v_idx on btg
+(3 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, v ORDER BY p, v;
+ QUERY PLAN
+------------------------------------------------
+ GroupAggregate
+ Group Key: p, v
+ -> Index Only Scan using btg_p_v_idx on btg
+(3 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p;
+ QUERY PLAN
+------------------------------------------------
+ GroupAggregate
+ Group Key: p, v
+ -> Index Only Scan using btg_p_v_idx on btg
+(3 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p ORDER BY p, v;
+ QUERY PLAN
+------------------------------------------------
+ GroupAggregate
+ Group Key: p, v
+ -> Index Only Scan using btg_p_v_idx on btg
+(3 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, c;
+ QUERY PLAN
+-------------------------------------------------
+ GroupAggregate
+ Group Key: p, c, v
+ -> Incremental Sort
+ Sort Key: p, c, v
+ Presorted Key: p
+ -> Index Scan using btg_p_v_idx on btg
+(6 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, c ORDER BY p, v;
+ QUERY PLAN
+-------------------------------------------------
+ GroupAggregate
+ Group Key: p, v, c
+ -> Incremental Sort
+ Sort Key: p, v, c
+ Presorted Key: p, v
+ -> Index Scan using btg_p_v_idx on btg
+(6 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, c, p, d;
+ QUERY PLAN
+-------------------------------------------------
+ GroupAggregate
+ Group Key: p, c, v, d
+ -> Incremental Sort
+ Sort Key: p, c, v, d
+ Presorted Key: p
+ -> Index Scan using btg_p_v_idx on btg
+(6 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, c, p, d ORDER BY p, v;
+ QUERY PLAN
+-------------------------------------------------
+ GroupAggregate
+ Group Key: p, v, c, d
+ -> Incremental Sort
+ Sort Key: p, v, c, d
+ Presorted Key: p, v
+ -> Index Scan using btg_p_v_idx on btg
+(6 rows)
+
+DROP TABLE btg;
+RESET enable_hashagg;
+RESET max_parallel_workers;
+RESET max_parallel_workers_per_gather;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
-- Secondly test the case of a parallel aggregate combiner function
-- returning NULL. For that use normal transition function, but a
-- combiner function returning NULL.
diff --git a/src/test/regress/expected/incremental_sort.out b/src/test/regress/expected/incremental_sort.out
index 7fdb685313..b8a0968b06 100644
--- a/src/test/regress/expected/incremental_sort.out
+++ b/src/test/regress/expected/incremental_sort.out
@@ -1426,7 +1426,7 @@ set parallel_setup_cost = 0;
set parallel_tuple_cost = 0;
set max_parallel_workers_per_gather = 2;
create table t (a int, b int, c int);
-insert into t select mod(i,10),mod(i,10),i from generate_series(1,10000) s(i);
+insert into t select mod(i,10),mod(i,10),i from generate_series(1,60000) s(i);
create index on t (a);
analyze t;
set enable_incremental_sort = off;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 9b8638f286..27684bfce2 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -1984,8 +1984,8 @@ USING (name);
------+----+----
bb | 12 | 13
cc | 22 | 23
- dd | | 33
ee | 42 |
+ dd | | 33
(4 rows)
-- Cases with non-nullable expressions in subquery results;
@@ -2019,8 +2019,8 @@ NATURAL FULL JOIN
------+------+------+------+------
bb | 12 | 2 | 13 | 3
cc | 22 | 2 | 23 | 3
- dd | | | 33 | 3
ee | 42 | 2 | |
+ dd | | | 33 | 3
(4 rows)
SELECT * FROM
@@ -5645,18 +5645,20 @@ select d.* from d left join (select * from b group by b.id, b.c_id) s
explain (costs off)
select d.* from d left join (select distinct * from b) s
on d.a = s.id;
- QUERY PLAN
---------------------------------------
- Merge Right Join
- Merge Cond: (b.id = d.a)
- -> Unique
- -> Sort
- Sort Key: b.id, b.c_id
- -> Seq Scan on b
+ QUERY PLAN
+---------------------------------------------
+ Merge Left Join
+ Merge Cond: (d.a = s.id)
-> Sort
Sort Key: d.a
-> Seq Scan on d
-(9 rows)
+ -> Sort
+ Sort Key: s.id
+ -> Subquery Scan on s
+ -> HashAggregate
+ Group Key: b.id, b.c_id
+ -> Seq Scan on b
+(11 rows)
-- join removal is not possible here
explain (costs off)
@@ -7597,44 +7599,39 @@ select * from j1 natural join j2;
explain (verbose, costs off)
select * from j1
inner join (select distinct id from j3) j3 on j1.id = j3.id;
- QUERY PLAN
------------------------------------------
+ QUERY PLAN
+-----------------------------------
Nested Loop
Output: j1.id, j3.id
Inner Unique: true
Join Filter: (j1.id = j3.id)
- -> Unique
+ -> HashAggregate
Output: j3.id
- -> Sort
+ Group Key: j3.id
+ -> Seq Scan on public.j3
Output: j3.id
- Sort Key: j3.id
- -> Seq Scan on public.j3
- Output: j3.id
-> Seq Scan on public.j1
Output: j1.id
-(13 rows)
+(11 rows)
-- ensure group by clause allows the inner to become unique
explain (verbose, costs off)
select * from j1
inner join (select id from j3 group by id) j3 on j1.id = j3.id;
- QUERY PLAN
------------------------------------------
+ QUERY PLAN
+-----------------------------------
Nested Loop
Output: j1.id, j3.id
Inner Unique: true
Join Filter: (j1.id = j3.id)
- -> Group
+ -> HashAggregate
Output: j3.id
Group Key: j3.id
- -> Sort
+ -> Seq Scan on public.j3
Output: j3.id
- Sort Key: j3.id
- -> Seq Scan on public.j3
- Output: j3.id
-> Seq Scan on public.j1
Output: j1.id
-(14 rows)
+(11 rows)
drop table j1;
drop table j2;
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
index 133d42117c..f7c4cc4833 100644
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -1459,18 +1459,15 @@ WHEN MATCHED AND t.a < 10 THEN
explain_merge
--------------------------------------------------------------------
Merge on ex_mtarget t (actual rows=0 loops=1)
- -> Merge Join (actual rows=0 loops=1)
- Merge Cond: (t.a = s.a)
- -> Sort (actual rows=0 loops=1)
- Sort Key: t.a
- Sort Method: quicksort Memory: xxx
+ -> Hash Join (actual rows=0 loops=1)
+ Hash Cond: (s.a = t.a)
+ -> Seq Scan on ex_msource s (actual rows=1 loops=1)
+ -> Hash (actual rows=0 loops=1)
+ Buckets: xxx Batches: xxx Memory Usage: xxx
-> Seq Scan on ex_mtarget t (actual rows=0 loops=1)
Filter: (a < '-1000'::integer)
Rows Removed by Filter: 54
- -> Sort (never executed)
- Sort Key: s.a
- -> Seq Scan on ex_msource s (never executed)
-(12 rows)
+(9 rows)
DROP TABLE ex_msource, ex_mtarget;
DROP FUNCTION explain_merge(text);
diff --git a/src/test/regress/expected/partition_aggregate.out b/src/test/regress/expected/partition_aggregate.out
index 1b900fddf8..800114dfde 100644
--- a/src/test/regress/expected/partition_aggregate.out
+++ b/src/test/regress/expected/partition_aggregate.out
@@ -948,12 +948,12 @@ SET parallel_setup_cost = 0;
-- is not partial agg safe.
EXPLAIN (COSTS OFF)
SELECT a, sum(b), array_agg(distinct c), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER BY 1, 2, 3;
- QUERY PLAN
---------------------------------------------------------------------------------------
- Sort
- Sort Key: pagg_tab_ml.a, (sum(pagg_tab_ml.b)), (array_agg(DISTINCT pagg_tab_ml.c))
- -> Gather
- Workers Planned: 2
+ QUERY PLAN
+--------------------------------------------------------------------------------------------
+ Gather Merge
+ Workers Planned: 2
+ -> Sort
+ Sort Key: pagg_tab_ml.a, (sum(pagg_tab_ml.b)), (array_agg(DISTINCT pagg_tab_ml.c))
-> Parallel Append
-> GroupAggregate
Group Key: pagg_tab_ml.a
@@ -1380,28 +1380,26 @@ SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) <
-- When GROUP BY clause does not match; partial aggregation is performed for each partition.
EXPLAIN (COSTS OFF)
SELECT y, sum(x), avg(x), count(*) FROM pagg_tab_para GROUP BY y HAVING avg(x) < 12 ORDER BY 1, 2, 3;
- QUERY PLAN
--------------------------------------------------------------------------------------------
+ QUERY PLAN
+-------------------------------------------------------------------------------------
Sort
Sort Key: pagg_tab_para.y, (sum(pagg_tab_para.x)), (avg(pagg_tab_para.x))
- -> Finalize GroupAggregate
+ -> Finalize HashAggregate
Group Key: pagg_tab_para.y
Filter: (avg(pagg_tab_para.x) < '12'::numeric)
- -> Gather Merge
+ -> Gather
Workers Planned: 2
- -> Sort
- Sort Key: pagg_tab_para.y
- -> Parallel Append
- -> Partial HashAggregate
- Group Key: pagg_tab_para.y
- -> Parallel Seq Scan on pagg_tab_para_p1 pagg_tab_para
- -> Partial HashAggregate
- Group Key: pagg_tab_para_1.y
- -> Parallel Seq Scan on pagg_tab_para_p2 pagg_tab_para_1
- -> Partial HashAggregate
- Group Key: pagg_tab_para_2.y
- -> Parallel Seq Scan on pagg_tab_para_p3 pagg_tab_para_2
-(19 rows)
+ -> Parallel Append
+ -> Partial HashAggregate
+ Group Key: pagg_tab_para.y
+ -> Parallel Seq Scan on pagg_tab_para_p1 pagg_tab_para
+ -> Partial HashAggregate
+ Group Key: pagg_tab_para_1.y
+ -> Parallel Seq Scan on pagg_tab_para_p2 pagg_tab_para_1
+ -> Partial HashAggregate
+ Group Key: pagg_tab_para_2.y
+ -> Parallel Seq Scan on pagg_tab_para_p3 pagg_tab_para_2
+(17 rows)
SELECT y, sum(x), avg(x), count(*) FROM pagg_tab_para GROUP BY y HAVING avg(x) < 12 ORDER BY 1, 2, 3;
y | sum | avg | count
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 6560fe2416..680e82cfc5 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -512,52 +512,41 @@ EXPLAIN (COSTS OFF)
SELECT a, b FROM prt1 FULL JOIN prt2 p2(b,a,c) USING(a,b)
WHERE a BETWEEN 490 AND 510
GROUP BY 1, 2 ORDER BY 1, 2;
- QUERY PLAN
------------------------------------------------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------
Group
Group Key: (COALESCE(prt1.a, p2.a)), (COALESCE(prt1.b, p2.b))
- -> Merge Append
+ -> Sort
Sort Key: (COALESCE(prt1.a, p2.a)), (COALESCE(prt1.b, p2.b))
- -> Group
- Group Key: (COALESCE(prt1.a, p2.a)), (COALESCE(prt1.b, p2.b))
- -> Sort
- Sort Key: (COALESCE(prt1.a, p2.a)), (COALESCE(prt1.b, p2.b))
- -> Merge Full Join
- Merge Cond: ((prt1.a = p2.a) AND (prt1.b = p2.b))
- Filter: ((COALESCE(prt1.a, p2.a) >= 490) AND (COALESCE(prt1.a, p2.a) <= 510))
- -> Sort
- Sort Key: prt1.a, prt1.b
- -> Seq Scan on prt1_p1 prt1
- -> Sort
- Sort Key: p2.a, p2.b
- -> Seq Scan on prt2_p1 p2
- -> Group
- Group Key: (COALESCE(prt1_1.a, p2_1.a)), (COALESCE(prt1_1.b, p2_1.b))
- -> Sort
- Sort Key: (COALESCE(prt1_1.a, p2_1.a)), (COALESCE(prt1_1.b, p2_1.b))
- -> Merge Full Join
- Merge Cond: ((prt1_1.a = p2_1.a) AND (prt1_1.b = p2_1.b))
- Filter: ((COALESCE(prt1_1.a, p2_1.a) >= 490) AND (COALESCE(prt1_1.a, p2_1.a) <= 510))
- -> Sort
- Sort Key: prt1_1.a, prt1_1.b
- -> Seq Scan on prt1_p2 prt1_1
- -> Sort
- Sort Key: p2_1.a, p2_1.b
- -> Seq Scan on prt2_p2 p2_1
- -> Group
- Group Key: (COALESCE(prt1_2.a, p2_2.a)), (COALESCE(prt1_2.b, p2_2.b))
- -> Sort
- Sort Key: (COALESCE(prt1_2.a, p2_2.a)), (COALESCE(prt1_2.b, p2_2.b))
- -> Merge Full Join
- Merge Cond: ((prt1_2.a = p2_2.a) AND (prt1_2.b = p2_2.b))
- Filter: ((COALESCE(prt1_2.a, p2_2.a) >= 490) AND (COALESCE(prt1_2.a, p2_2.a) <= 510))
- -> Sort
- Sort Key: prt1_2.a, prt1_2.b
- -> Seq Scan on prt1_p3 prt1_2
- -> Sort
- Sort Key: p2_2.a, p2_2.b
- -> Seq Scan on prt2_p3 p2_2
-(43 rows)
+ -> Append
+ -> Merge Full Join
+ Merge Cond: ((prt1_1.a = p2_1.a) AND (prt1_1.b = p2_1.b))
+ Filter: ((COALESCE(prt1_1.a, p2_1.a) >= 490) AND (COALESCE(prt1_1.a, p2_1.a) <= 510))
+ -> Sort
+ Sort Key: prt1_1.a, prt1_1.b
+ -> Seq Scan on prt1_p1 prt1_1
+ -> Sort
+ Sort Key: p2_1.a, p2_1.b
+ -> Seq Scan on prt2_p1 p2_1
+ -> Merge Full Join
+ Merge Cond: ((prt1_2.a = p2_2.a) AND (prt1_2.b = p2_2.b))
+ Filter: ((COALESCE(prt1_2.a, p2_2.a) >= 490) AND (COALESCE(prt1_2.a, p2_2.a) <= 510))
+ -> Sort
+ Sort Key: prt1_2.a, prt1_2.b
+ -> Seq Scan on prt1_p2 prt1_2
+ -> Sort
+ Sort Key: p2_2.a, p2_2.b
+ -> Seq Scan on prt2_p2 p2_2
+ -> Merge Full Join
+ Merge Cond: ((prt1_3.a = p2_3.a) AND (prt1_3.b = p2_3.b))
+ Filter: ((COALESCE(prt1_3.a, p2_3.a) >= 490) AND (COALESCE(prt1_3.a, p2_3.a) <= 510))
+ -> Sort
+ Sort Key: prt1_3.a, prt1_3.b
+ -> Seq Scan on prt1_p3 prt1_3
+ -> Sort
+ Sort Key: p2_3.a, p2_3.b
+ -> Seq Scan on prt2_p3 p2_3
+(32 rows)
SELECT a, b FROM prt1 FULL JOIN prt2 p2(b,a,c) USING(a,b)
WHERE a BETWEEN 490 AND 510
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index aae5d51e1c..487fe26906 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -114,6 +114,7 @@ select name, setting from pg_settings where name like 'enable%';
enable_async_append | on
enable_bitmapscan | on
enable_gathermerge | on
+ enable_group_by_reordering | on
enable_hashagg | on
enable_hashjoin | on
enable_incremental_sort | on
@@ -132,7 +133,7 @@ select name, setting from pg_settings where name like 'enable%';
enable_seqscan | on
enable_sort | on
enable_tidscan | on
-(21 rows)
+(22 rows)
-- There are always wait event descriptions for various types.
select type, count(*) > 0 as ok FROM pg_wait_events
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index e2613d6777..220f0c1629 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -1303,24 +1303,22 @@ select distinct q1 from
union all
select distinct * from int8_tbl i82) ss
where q2 = q2;
- QUERY PLAN
-----------------------------------------------------------
- Unique
- -> Merge Append
- Sort Key: "*SELECT* 1".q1
+ QUERY PLAN
+----------------------------------------------------
+ HashAggregate
+ Group Key: "*SELECT* 1".q1
+ -> Append
-> Subquery Scan on "*SELECT* 1"
- -> Unique
- -> Sort
- Sort Key: i81.q1, i81.q2
- -> Seq Scan on int8_tbl i81
- Filter: (q2 IS NOT NULL)
+ -> HashAggregate
+ Group Key: i81.q1, i81.q2
+ -> Seq Scan on int8_tbl i81
+ Filter: (q2 IS NOT NULL)
-> Subquery Scan on "*SELECT* 2"
- -> Unique
- -> Sort
- Sort Key: i82.q1, i82.q2
- -> Seq Scan on int8_tbl i82
- Filter: (q2 IS NOT NULL)
-(15 rows)
+ -> HashAggregate
+ Group Key: i82.q1, i82.q2
+ -> Seq Scan on int8_tbl i82
+ Filter: (q2 IS NOT NULL)
+(13 rows)
select distinct q1 from
(select distinct * from int8_tbl i81
@@ -1339,24 +1337,22 @@ select distinct q1 from
union all
select distinct * from int8_tbl i82) ss
where -q1 = q2;
- QUERY PLAN
---------------------------------------------------------
- Unique
- -> Merge Append
- Sort Key: "*SELECT* 1".q1
+ QUERY PLAN
+--------------------------------------------------
+ HashAggregate
+ Group Key: "*SELECT* 1".q1
+ -> Append
-> Subquery Scan on "*SELECT* 1"
- -> Unique
- -> Sort
- Sort Key: i81.q1, i81.q2
- -> Seq Scan on int8_tbl i81
- Filter: ((- q1) = q2)
+ -> HashAggregate
+ Group Key: i81.q1, i81.q2
+ -> Seq Scan on int8_tbl i81
+ Filter: ((- q1) = q2)
-> Subquery Scan on "*SELECT* 2"
- -> Unique
- -> Sort
- Sort Key: i82.q1, i82.q2
- -> Seq Scan on int8_tbl i82
- Filter: ((- q1) = q2)
-(15 rows)
+ -> HashAggregate
+ Group Key: i82.q1, i82.q2
+ -> Seq Scan on int8_tbl i82
+ Filter: ((- q1) = q2)
+(13 rows)
select distinct q1 from
(select distinct * from int8_tbl i81
diff --git a/src/test/regress/sql/aggregates.sql b/src/test/regress/sql/aggregates.sql
index 75c78be640..5c4e768825 100644
--- a/src/test/regress/sql/aggregates.sql
+++ b/src/test/regress/sql/aggregates.sql
@@ -1172,6 +1172,105 @@ SELECT balk(hundred) FROM tenk1;
ROLLBACK;
+-- GROUP BY optimization by reorder columns
+
+SELECT
+ i AS id,
+ i/2 AS p,
+ format('%60s', i%2) AS v,
+ i/4 AS c,
+ i/8 AS d,
+ (random() * (10000/8))::int as e --the same as d but no correlation with p
+ INTO btg
+FROM
+ generate_series(1, 10000) i;
+
+VACUUM btg;
+ANALYZE btg;
+
+-- GROUP BY optimization by reorder columns by frequency
+
+SET enable_hashagg=off;
+SET max_parallel_workers= 0;
+SET max_parallel_workers_per_gather = 0;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, v;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, c;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, c ORDER BY v, p, c;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, d, c;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, d, c ORDER BY v, p, d ,c;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, d, c ORDER BY p, v, d ,c;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, d, e;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, e, d;
+
+CREATE STATISTICS btg_dep ON d, e, p FROM btg;
+ANALYZE btg;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, d, e;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, e, d;
+
+
+-- GROUP BY optimization by reorder columns by index scan
+
+CREATE INDEX ON btg(p, v);
+SET enable_seqscan=off;
+SET enable_bitmapscan=off;
+VACUUM btg;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, v;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, v ORDER BY p, v;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p ORDER BY p, v;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, c;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, c ORDER BY p, v;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, c, p, d;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, c, p, d ORDER BY p, v;
+
+DROP TABLE btg;
+
+RESET enable_hashagg;
+RESET max_parallel_workers;
+RESET max_parallel_workers_per_gather;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+
-- Secondly test the case of a parallel aggregate combiner function
-- returning NULL. For that use normal transition function, but a
-- combiner function returning NULL.
diff --git a/src/test/regress/sql/incremental_sort.sql b/src/test/regress/sql/incremental_sort.sql
index ab471bdfff..70f311a6f2 100644
--- a/src/test/regress/sql/incremental_sort.sql
+++ b/src/test/regress/sql/incremental_sort.sql
@@ -208,7 +208,7 @@ set parallel_tuple_cost = 0;
set max_parallel_workers_per_gather = 2;
create table t (a int, b int, c int);
-insert into t select mod(i,10),mod(i,10),i from generate_series(1,10000) s(i);
+insert into t select mod(i,10),mod(i,10),i from generate_series(1,60000) s(i);
create index on t (a);
analyze t;
--
2.42.0
-- join with pseudoconstant quals
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1 AND CURRENT_USER = SESSION_USER) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------
Limit
Output: t1.c1, t2.c1, t1.c3
- -> Result
+ -> Sort
Output: t1.c1, t2.c1, t1.c3
- One-Time Filter: (CURRENT_USER = SESSION_USER)
- -> Foreign Scan
- Output: t1.c1, t1.c3, t2.c1
- Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
- Remote SQL: SELECT r1."C 1", r2."C 1", r1.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r1."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
-(9 rows)
+ Sort Key: t1.c3, t1.c1
+ -> Result
+ Output: t1.c1, t2.c1, t1.c3
+ One-Time Filter: (CURRENT_USER = SESSION_USER)
+ -> Foreign Scan
+ Output: t1.c1, t1.c3, t2.c1
+ Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
+ Remote SQL: SELECT r1."C 1", r2."C 1", r1.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r1."C 1"))))
+(12 rows)
-- non-Var items in targetlist of the nullable rel of a join preventing
-- push-down in some cases
Attachments:
[text/plain] 0001-When-evaluating-a-query-with-a-multi-column-GROUP-BY-20230913.patch (97.4K, ../../[email protected]/2-0001-When-evaluating-a-query-with-a-multi-column-GROUP-BY-20230913.patch)
download | inline diff:
From 33953655c9ac3f9ec64b80c9f2a2ff38bd178745 Mon Sep 17 00:00:00 2001
From: "Andrey V. Lepikhov" <[email protected]>
Date: Wed, 13 Sep 2023 11:20:03 +0700
Subject: [PATCH] Explore alternative orderings of group-by pathkeys during
optimization.
When evaluating a query with a multi-column GROUP BY clause using sort,
the cost may depend heavily on the order in which the keys are compared when
building the groups. Grouping does not imply any ordering, so we can compare
the keys in arbitrary order, and a Hash Agg leverages this. But for Group Agg,
we simply compared keys in the order specified in the query. This commit
explores alternative ordering of the keys, trying to find a cheaper one.
In principle, we might generate grouping paths for all permutations of the keys
and leave the rest to the optimizer. But that might get very expensive, so we
try to pick only a couple interesting orderings based on both local and global
information.
When planning the grouping path, we explore statistics (number of distinct
values, cost of the comparison function) for the keys and reorder them
to minimize comparison costs. Intuitively, it may be better to perform more
expensive comparisons (for complex data types, etc.) last because maybe
the cheaper comparisons will be enough. Similarly, the higher the cardinality
of a key, the lower the probability we'll need to compare more keys. The patch
generates and costs various orderings, picking the cheapest ones.
The ordering of group keys may interact with other parts of the query, some of
which may not be known while planning the grouping. For example, there may be
an explicit ORDER BY clause or some other ordering-dependent operation higher up
in the query, and using the same ordering may allow using either incremental
sort or even eliminating the sort entirely.
The patch generates orderings and picks those, minimizing the comparison cost
(for various path keys), and then adds orderings that might be useful for
operations higher up in the plan (ORDER BY, etc.). Finally, it always keeps
the ordering specified in the query, assuming the user might have additional
insights.
This introduces a new GUC enable_group_by_reordering so that the optimization
may be disabled if needed.
---
.../postgres_fdw/expected/postgres_fdw.out | 36 +-
src/backend/optimizer/path/costsize.c | 363 ++++++++++-
src/backend/optimizer/path/equivclass.c | 13 +-
src/backend/optimizer/path/pathkeys.c | 602 ++++++++++++++++++
src/backend/optimizer/plan/planner.c | 465 ++++++++------
src/backend/optimizer/util/pathnode.c | 4 +-
src/backend/utils/adt/selfuncs.c | 38 +-
src/backend/utils/misc/guc_tables.c | 10 +
src/backend/utils/misc/postgresql.conf.sample | 1 +
src/include/nodes/pathnodes.h | 10 +
src/include/optimizer/cost.h | 4 +-
src/include/optimizer/paths.h | 7 +
src/include/utils/selfuncs.h | 5 +
src/test/regress/expected/aggregates.out | 244 ++++++-
.../regress/expected/incremental_sort.out | 2 +-
src/test/regress/expected/join.out | 51 +-
src/test/regress/expected/merge.out | 15 +-
.../regress/expected/partition_aggregate.out | 44 +-
src/test/regress/expected/partition_join.out | 75 +--
src/test/regress/expected/sysviews.out | 3 +-
src/test/regress/expected/union.out | 60 +-
src/test/regress/sql/aggregates.sql | 99 +++
src/test/regress/sql/incremental_sort.sql | 2 +-
23 files changed, 1771 insertions(+), 382 deletions(-)
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 144c114d0f..63af7feabe 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -2319,18 +2319,21 @@ SELECT t1."C 1" FROM "S 1"."T 1" t1, LATERAL (SELECT DISTINCT t2.c1, t3.c1 FROM
-- join with pseudoconstant quals
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1 AND CURRENT_USER = SESSION_USER) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------
Limit
Output: t1.c1, t2.c1, t1.c3
- -> Result
+ -> Sort
Output: t1.c1, t2.c1, t1.c3
- One-Time Filter: (CURRENT_USER = SESSION_USER)
- -> Foreign Scan
- Output: t1.c1, t1.c3, t2.c1
- Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
- Remote SQL: SELECT r1."C 1", r2."C 1", r1.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r1."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
-(9 rows)
+ Sort Key: t1.c3, t1.c1
+ -> Result
+ Output: t1.c1, t2.c1, t1.c3
+ One-Time Filter: (CURRENT_USER = SESSION_USER)
+ -> Foreign Scan
+ Output: t1.c1, t1.c3, t2.c1
+ Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
+ Remote SQL: SELECT r1."C 1", r2."C 1", r1.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r1."C 1"))))
+(12 rows)
-- non-Var items in targetlist of the nullable rel of a join preventing
-- push-down in some cases
@@ -9609,13 +9612,16 @@ SELECT t1.a,t2.b,t3.c FROM fprt1 t1 INNER JOIN fprt2 t2 ON (t1.a = t2.b) INNER J
-- left outer join + nullable clause
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.a,t2.b,t2.c FROM fprt1 t1 LEFT JOIN (SELECT * FROM fprt2 WHERE a < 10) t2 ON (t1.a = t2.b and t1.b = t2.a) WHERE t1.a < 10 ORDER BY 1,2,3;
- QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Foreign Scan
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Sort
Output: t1.a, fprt2.b, fprt2.c
- Relations: (public.ftprt1_p1 t1) LEFT JOIN (public.ftprt2_p1 fprt2)
- Remote SQL: SELECT r5.a, r6.b, r6.c FROM (public.fprt1_p1 r5 LEFT JOIN public.fprt2_p1 r6 ON (((r5.a = r6.b)) AND ((r5.b = r6.a)) AND ((r6.a < 10)))) WHERE ((r5.a < 10)) ORDER BY r5.a ASC NULLS LAST, r6.b ASC NULLS LAST, r6.c ASC NULLS LAST
-(4 rows)
+ Sort Key: t1.a, fprt2.b, fprt2.c
+ -> Foreign Scan
+ Output: t1.a, fprt2.b, fprt2.c
+ Relations: (public.ftprt1_p1 t1) LEFT JOIN (public.ftprt2_p1 fprt2)
+ Remote SQL: SELECT r5.a, r6.b, r6.c FROM (public.fprt1_p1 r5 LEFT JOIN public.fprt2_p1 r6 ON (((r5.a = r6.b)) AND ((r5.b = r6.a)) AND ((r6.a < 10)))) WHERE ((r5.a < 10))
+(7 rows)
SELECT t1.a,t2.b,t2.c FROM fprt1 t1 LEFT JOIN (SELECT * FROM fprt2 WHERE a < 10) t2 ON (t1.a = t2.b and t1.b = t2.a) WHERE t1.a < 10 ORDER BY 1,2,3;
a | b | c
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index d6ceafd51c..6f11f679ba 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -1816,6 +1816,327 @@ cost_recursive_union(Path *runion, Path *nrterm, Path *rterm)
rterm->pathtarget->width);
}
+/*
+ * is_fake_var
+ * Workaround for generate_append_tlist() which generates fake Vars with
+ * varno == 0, that will cause a fail of estimate_num_group() call
+ *
+ * XXX Ummm, why would estimate_num_group fail with this?
+ */
+static bool
+is_fake_var(Expr *expr)
+{
+ if (IsA(expr, RelabelType))
+ expr = (Expr *) ((RelabelType *) expr)->arg;
+
+ return (IsA(expr, Var) && ((Var *) expr)->varno == 0);
+}
+
+/*
+ * get_width_cost_multiplier
+ * Returns relative complexity of comparing two values based on its width.
+ * The idea behind is that the comparison becomes more expensive the longer the
+ * value is. Return value is in cpu_operator_cost units.
+ */
+static double
+get_width_cost_multiplier(PlannerInfo *root, Expr *expr)
+{
+ double width = -1.0; /* fake value */
+
+ if (IsA(expr, RelabelType))
+ expr = (Expr *) ((RelabelType *) expr)->arg;
+
+ /* Try to find actual stat in corresponding relation */
+ if (IsA(expr, Var))
+ {
+ Var *var = (Var *) expr;
+
+ if (var->varno > 0 && var->varno < root->simple_rel_array_size)
+ {
+ RelOptInfo *rel = root->simple_rel_array[var->varno];
+
+ if (rel != NULL &&
+ var->varattno >= rel->min_attr &&
+ var->varattno <= rel->max_attr)
+ {
+ int ndx = var->varattno - rel->min_attr;
+
+ if (rel->attr_widths[ndx] > 0)
+ width = rel->attr_widths[ndx];
+ }
+ }
+ }
+
+ /* Didn't find any actual stats, try using type width instead. */
+ if (width < 0.0)
+ {
+ Node *node = (Node *) expr;
+
+ width = get_typavgwidth(exprType(node), exprTypmod(node));
+ }
+
+ /*
+ * Values are passed as Datum type, so comparisons can't be cheaper than
+ * comparing a Datum value.
+ *
+ * FIXME I find this reasoning questionable. We may pass int2, and
+ * comparing it is probably a bit cheaper than comparing a bigint.
+ */
+ if (width <= sizeof(Datum))
+ return 1.0;
+
+ /*
+ * We consider the cost of a comparison not to be directly proportional to
+ * width of the argument, because widths of the arguments could be
+ * slightly different (we only know the average width for the whole
+ * column). So we use log16(width) as an estimate.
+ */
+ return 1.0 + 0.125 * LOG2(width / sizeof(Datum));
+}
+
+/*
+ * compute_cpu_sort_cost
+ * compute CPU cost of sort (i.e. in-memory)
+ *
+ * The main thing we need to calculate to estimate sort CPU costs is the number
+ * of calls to the comparator functions. The difficulty is that for multi-column
+ * sorts there may be different data types involved (for some of which the calls
+ * may be much more expensive). Furthermore, columns may have a very different
+ * number of distinct values - the higher the number, the fewer comparisons will
+ * be needed for the following columns.
+ *
+ * The algorithm is incremental - we add pathkeys one by one, and at each step we
+ * estimate the number of necessary comparisons (based on the number of distinct
+ * groups in the current pathkey prefix and the new pathkey), and the comparison
+ * costs (which is data type specific).
+ *
+ * Estimation of the number of comparisons is based on ideas from:
+ *
+ * "Quicksort Is Optimal", Robert Sedgewick, Jon Bentley, 2002
+ * [https://www.cs.princeton.edu/~rs/talks/QuicksortIsOptimal.pdf]
+ *
+ * In term of that paper, let N - number of tuples, Xi - number of identical
+ * tuples with value Ki, then the estimate of number of comparisons is:
+ *
+ * log(N! / (X1! * X2! * ..)) ~ sum(Xi * log(N/Xi))
+ *
+ * We assume all Xi the same because now we don't have any estimation of
+ * group sizes, we have only know the estimate of number of groups (distinct
+ * values). In that case, formula becomes:
+ *
+ * N * log(NumberOfGroups)
+ *
+ * For multi-column sorts we need to estimate the number of comparisons for
+ * each individual column - for example with columns (c1, c2, ..., ck) we
+ * can estimate that number of comparisons on ck is roughly
+ *
+ * ncomparisons(c1, c2, ..., ck) / ncomparisons(c1, c2, ..., c(k-1))
+ *
+ * Let k be a column number, Gk - number of groups defined by k columns, and Fk
+ * the cost of the comparison is
+ *
+ * N * sum( Fk * log(Gk) )
+ *
+ * Note: We also consider column width, not just the comparator cost.
+ *
+ * NOTE: some callers currently pass NIL for pathkeys because they
+ * can't conveniently supply the sort keys. In this case, it will fallback to
+ * simple comparison cost estimate.
+ */
+static Cost
+compute_cpu_sort_cost(PlannerInfo *root, List *pathkeys, int nPresortedKeys,
+ Cost comparison_cost, double tuples, double output_tuples,
+ bool heapSort)
+{
+ Cost per_tuple_cost = 0.0;
+ ListCell *lc;
+ List *pathkeyExprs = NIL;
+ double tuplesPerPrevGroup = tuples;
+ double totalFuncCost = 1.0;
+ bool has_fake_var = false;
+ int i = 0;
+ Oid prev_datatype = InvalidOid;
+ List *cache_varinfos = NIL;
+
+ /* fallback if pathkeys is unknown */
+ if (list_length(pathkeys) == 0)
+ {
+ /*
+ * If we'll use a bounded heap-sort keeping just K tuples in memory,
+ * for a total number of tuple comparisons of N log2 K; but the
+ * constant factor is a bit higher than for quicksort. Tweak it so
+ * that the cost curve is continuous at the crossover point.
+ */
+ output_tuples = (heapSort) ? 2.0 * output_tuples : tuples;
+ per_tuple_cost += 2.0 * cpu_operator_cost * LOG2(output_tuples);
+
+ /* add cost provided by caller */
+ per_tuple_cost += comparison_cost;
+
+ return per_tuple_cost * tuples;
+ }
+
+ /*
+ * Computing total cost of sorting takes into account the per-column
+ * comparison function cost. We try to compute the needed number of
+ * comparisons per column.
+ */
+ foreach(lc, pathkeys)
+ {
+ PathKey *pathkey = (PathKey *) lfirst(lc);
+ EquivalenceMember *em;
+ double nGroups,
+ correctedNGroups;
+ Cost funcCost = 1.0;
+
+ /*
+ * We believe that equivalence members aren't very different, so, to
+ * estimate cost we consider just the first member.
+ */
+ em = (EquivalenceMember *) linitial(pathkey->pk_eclass->ec_members);
+
+ if (em->em_datatype != InvalidOid)
+ {
+ /* do not lookup funcCost if the data type is the same */
+ if (prev_datatype != em->em_datatype)
+ {
+ Oid sortop;
+ QualCost cost;
+
+ sortop = get_opfamily_member(pathkey->pk_opfamily,
+ em->em_datatype, em->em_datatype,
+ pathkey->pk_strategy);
+
+ cost.startup = 0;
+ cost.per_tuple = 0;
+ add_function_cost(root, get_opcode(sortop), NULL, &cost);
+
+ /*
+ * add_function_cost returns the product of cpu_operator_cost
+ * and procost, but we need just procost, co undo that.
+ */
+ funcCost = cost.per_tuple / cpu_operator_cost;
+
+ prev_datatype = em->em_datatype;
+ }
+ }
+
+ /* factor in the width of the values in this column */
+ funcCost *= get_width_cost_multiplier(root, em->em_expr);
+
+ /* now we have per-key cost, so add to the running total */
+ totalFuncCost += funcCost;
+
+ /* remember if we have found a fake Var in pathkeys */
+ has_fake_var |= is_fake_var(em->em_expr);
+ pathkeyExprs = lappend(pathkeyExprs, em->em_expr);
+
+ /*
+ * We need to calculate the number of comparisons for this column,
+ * which requires knowing the group size. So we estimate the number of
+ * groups by calling estimate_num_groups_incremental(), which
+ * estimates the group size for "new" pathkeys.
+ *
+ * Note: estimate_num_groups_incremental does not handle fake Vars, so
+ * use a default estimate otherwise.
+ */
+ if (!has_fake_var)
+ nGroups = estimate_num_groups_incremental(root, pathkeyExprs,
+ tuplesPerPrevGroup, NULL, NULL,
+ &cache_varinfos,
+ list_length(pathkeyExprs) - 1);
+ else if (tuples > 4.0)
+
+ /*
+ * Use geometric mean as estimation if there are no stats.
+ *
+ * We don't use DEFAULT_NUM_DISTINCT here, because that's used for
+ * a single column, but here we're dealing with multiple columns.
+ */
+ nGroups = ceil(2.0 + sqrt(tuples) * (i + 1) / list_length(pathkeys));
+ else
+ nGroups = tuples;
+
+ /*
+ * Presorted keys are not considered in the cost above, but we still
+ * do have to compare them in the qsort comparator. So make sure to
+ * factor in the cost in that case.
+ */
+ if (i >= nPresortedKeys)
+ {
+ if (heapSort)
+ {
+ /*
+ * have to keep at least one group, and a multiple of group
+ * size
+ */
+ correctedNGroups = ceil(output_tuples / tuplesPerPrevGroup);
+ }
+ else
+ /* all groups in the input */
+ correctedNGroups = nGroups;
+
+ correctedNGroups = Max(1.0, ceil(correctedNGroups));
+
+ per_tuple_cost += totalFuncCost * LOG2(correctedNGroups);
+ }
+
+ i++;
+
+ /*
+ * Once we get single-row group, it means tuples in the group are
+ * unique and we can skip all remaining columns.
+ */
+ if (tuplesPerPrevGroup <= nGroups)
+ break;
+
+ /*
+ * Uniform distributions with all groups being of the same size are
+ * the best case, with nice smooth behavior. Real-world distributions
+ * tend not to be uniform, though, and we don't have any reliable
+ * easy-to-use information. As a basic defense against skewed
+ * distributions, we use a 1.5 factor to make the expected group a bit
+ * larger, but we need to be careful not to make the group larger than
+ * in the preceding step.
+ */
+ tuplesPerPrevGroup = Min(tuplesPerPrevGroup,
+ ceil(1.5 * tuplesPerPrevGroup / nGroups));
+ }
+
+ list_free(pathkeyExprs);
+
+ /* per_tuple_cost is in cpu_operator_cost units */
+ per_tuple_cost *= cpu_operator_cost;
+
+ /*
+ * Accordingly to "Introduction to algorithms", Thomas H. Cormen, Charles
+ * E. Leiserson, Ronald L. Rivest, ISBN 0-07-013143-0, quicksort
+ * estimation formula has additional term proportional to number of tuples
+ * (see Chapter 8.2 and Theorem 4.1). That affects cases with a low number
+ * of tuples, approximately less than 1e4. We could implement it as an
+ * additional multiplier under the logarithm, but we use a bit more
+ * complex formula which takes into account the number of unique tuples
+ * and it's not clear how to combine the multiplier with the number of
+ * groups. Estimate it as 10 cpu_operator_cost units.
+ */
+ per_tuple_cost += 10 * cpu_operator_cost;
+
+ per_tuple_cost += comparison_cost;
+
+ return tuples * per_tuple_cost;
+}
+
+/*
+ * simple wrapper just to estimate best sort path
+ */
+Cost
+cost_sort_estimate(PlannerInfo *root, List *pathkeys, int nPresortedKeys,
+ double tuples)
+{
+ return compute_cpu_sort_cost(root, pathkeys, nPresortedKeys,
+ 0, tuples, tuples, false);
+}
+
/*
* cost_tuplesort
* Determines and returns the cost of sorting a relation using tuplesort,
@@ -1832,7 +2153,7 @@ cost_recursive_union(Path *runion, Path *nrterm, Path *rterm)
* number of initial runs formed and M is the merge order used by tuplesort.c.
* Since the average initial run should be about sort_mem, we have
* disk traffic = 2 * relsize * ceil(logM(p / sort_mem))
- * cpu = comparison_cost * t * log2(t)
+ * and cpu cost (computed by compute_cpu_sort_cost()).
*
* If the sort is bounded (i.e., only the first k result tuples are needed)
* and k tuples can fit into sort_mem, we use a heap method that keeps only
@@ -1851,9 +2172,11 @@ cost_recursive_union(Path *runion, Path *nrterm, Path *rterm)
* 'comparison_cost' is the extra cost per comparison, if any
* 'sort_mem' is the number of kilobytes of work memory allowed for the sort
* 'limit_tuples' is the bound on the number of output tuples; -1 if no bound
+ * 'startup_cost' is expected to be 0 at input. If there is "input cost" it should
+ * be added by caller later
*/
static void
-cost_tuplesort(Cost *startup_cost, Cost *run_cost,
+cost_tuplesort(PlannerInfo *root, List *pathkeys, Cost *startup_cost, Cost *run_cost,
double tuples, int width,
Cost comparison_cost, int sort_mem,
double limit_tuples)
@@ -1870,9 +2193,6 @@ cost_tuplesort(Cost *startup_cost, Cost *run_cost,
if (tuples < 2.0)
tuples = 2.0;
- /* Include the default cost-per-comparison */
- comparison_cost += 2.0 * cpu_operator_cost;
-
/* Do we have a useful LIMIT? */
if (limit_tuples > 0 && limit_tuples < tuples)
{
@@ -1896,12 +2216,10 @@ cost_tuplesort(Cost *startup_cost, Cost *run_cost,
double log_runs;
double npageaccesses;
- /*
- * CPU costs
- *
- * Assume about N log2 N comparisons
- */
- *startup_cost = comparison_cost * tuples * LOG2(tuples);
+ /* CPU costs */
+ *startup_cost = compute_cpu_sort_cost(root, pathkeys, 0,
+ comparison_cost, tuples,
+ tuples, false);
/* Disk costs */
@@ -1917,18 +2235,17 @@ cost_tuplesort(Cost *startup_cost, Cost *run_cost,
}
else if (tuples > 2 * output_tuples || input_bytes > sort_mem_bytes)
{
- /*
- * We'll use a bounded heap-sort keeping just K tuples in memory, for
- * a total number of tuple comparisons of N log2 K; but the constant
- * factor is a bit higher than for quicksort. Tweak it so that the
- * cost curve is continuous at the crossover point.
- */
- *startup_cost = comparison_cost * tuples * LOG2(2.0 * output_tuples);
+ /* We'll use a bounded heap-sort keeping just K tuples in memory. */
+ *startup_cost = compute_cpu_sort_cost(root, pathkeys, 0,
+ comparison_cost, tuples,
+ output_tuples, true);
}
else
{
/* We'll use plain quicksort on all the input tuples */
- *startup_cost = comparison_cost * tuples * LOG2(tuples);
+ *startup_cost = compute_cpu_sort_cost(root, pathkeys, 0,
+ comparison_cost, tuples,
+ tuples, false);
}
/*
@@ -2042,7 +2359,7 @@ cost_incremental_sort(Path *path,
* Estimate the average cost of sorting of one group where presorted keys
* are equal.
*/
- cost_tuplesort(&group_startup_cost, &group_run_cost,
+ cost_tuplesort(root, pathkeys, &group_startup_cost, &group_run_cost,
group_tuples, width, comparison_cost, sort_mem,
limit_tuples);
@@ -2102,7 +2419,7 @@ cost_sort(Path *path, PlannerInfo *root,
Cost startup_cost;
Cost run_cost;
- cost_tuplesort(&startup_cost, &run_cost,
+ cost_tuplesort(root, pathkeys, &startup_cost, &run_cost,
tuples, width,
comparison_cost, sort_mem,
limit_tuples);
@@ -2200,7 +2517,7 @@ append_nonpartial_cost(List *subpaths, int numpaths, int parallel_workers)
* Determines and returns the cost of an Append node.
*/
void
-cost_append(AppendPath *apath)
+cost_append(AppendPath *apath, PlannerInfo *root)
{
ListCell *l;
@@ -2268,7 +2585,7 @@ cost_append(AppendPath *apath)
* any child.
*/
cost_sort(&sort_path,
- NULL, /* doesn't currently need root */
+ root,
pathkeys,
subpath->total_cost,
subpath->rows,
diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c
index 7fa502d6e2..07edd4f38e 100644
--- a/src/backend/optimizer/path/equivclass.c
+++ b/src/backend/optimizer/path/equivclass.c
@@ -652,7 +652,18 @@ get_eclass_for_sort_expr(PlannerInfo *root,
if (opcintype == cur_em->em_datatype &&
equal(expr, cur_em->em_expr))
- return cur_ec; /* Match! */
+ {
+ /*
+ * Match!
+ *
+ * Copy the sortref if it wasn't set yet. That may happen if
+ * the ec was constructed from WHERE clause, i.e. it doesn't
+ * have a target reference at all.
+ */
+ if (cur_ec->ec_sortref == 0 && sortref > 0)
+ cur_ec->ec_sortref = sortref;
+ return cur_ec;
+ }
}
}
diff --git a/src/backend/optimizer/path/pathkeys.c b/src/backend/optimizer/path/pathkeys.c
index fdb60aaa8d..2dfc983a6d 100644
--- a/src/backend/optimizer/path/pathkeys.c
+++ b/src/backend/optimizer/path/pathkeys.c
@@ -17,17 +17,24 @@
*/
#include "postgres.h"
+#include <float.h>
+
+#include "miscadmin.h"
#include "access/stratnum.h"
#include "catalog/pg_opfamily.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "nodes/plannodes.h"
+#include "optimizer/cost.h"
#include "optimizer/optimizer.h"
#include "optimizer/pathnode.h"
#include "optimizer/paths.h"
#include "partitioning/partbounds.h"
#include "utils/lsyscache.h"
+#include "utils/selfuncs.h"
+/* Consider reordering of GROUP BY keys? */
+bool enable_group_by_reordering = true;
static bool pathkey_is_redundant(PathKey *new_pathkey, List *pathkeys);
static bool matches_boolean_partition_clause(RestrictInfo *rinfo,
@@ -350,6 +357,548 @@ pathkeys_contained_in(List *keys1, List *keys2)
return false;
}
+/*
+ * group_keys_reorder_by_pathkeys
+ * Reorder GROUP BY keys to match pathkeys of input path.
+ *
+ * Function returns new lists (pathkeys and clauses), original GROUP BY lists
+ * stay untouched.
+ *
+ * Returns the number of GROUP BY keys with a matching pathkey.
+ */
+int
+group_keys_reorder_by_pathkeys(List *pathkeys, List **group_pathkeys,
+ List **group_clauses,
+ int num_groupby_pathkeys)
+{
+ List *new_group_pathkeys = NIL,
+ *new_group_clauses = NIL;
+ ListCell *lc;
+ int n;
+
+ if (pathkeys == NIL || *group_pathkeys == NIL)
+ return 0;
+
+ /*
+ * Walk the pathkeys (determining ordering of the input path) and see if
+ * there's a matching GROUP BY key. If we find one, we append it to the
+ * list, and do the same for the clauses.
+ *
+ * Once we find the first pathkey without a matching GROUP BY key, the
+ * rest of the pathkeys are useless and can't be used to evaluate the
+ * grouping, so we abort the loop and ignore the remaining pathkeys.
+ *
+ * XXX Pathkeys are built in a way to allow simply comparing pointers.
+ */
+ foreach(lc, pathkeys)
+ {
+ PathKey *pathkey = (PathKey *) lfirst(lc);
+ SortGroupClause *sgc;
+
+ if (foreach_current_index(lc) >= num_groupby_pathkeys ||
+ !list_member_ptr(*group_pathkeys, pathkey))
+ /* */
+ break;
+
+ /* abort on first mismatch */
+ sgc = get_sortgroupref_clause_noerr(pathkey->pk_eclass->ec_sortref,
+ *group_clauses);
+ if (!sgc)
+ /* The grouping clause is not cover this paathkey */
+ break;
+
+ new_group_pathkeys = lappend(new_group_pathkeys, pathkey);
+ new_group_clauses = lappend(new_group_clauses, sgc);
+ }
+
+ /* remember the number of pathkeys with a matching GROUP BY key */
+ n = list_length(new_group_pathkeys);
+
+ /* append the remaining group pathkeys (will be treated as not sorted) */
+ *group_pathkeys = list_concat_unique_ptr(new_group_pathkeys,
+ *group_pathkeys);
+ *group_clauses = list_concat_unique_ptr(new_group_clauses,
+ *group_clauses);
+
+ return n;
+}
+
+/*
+ * Used to generate all permutations of a pathkey list.
+ */
+typedef struct PathkeyMutatorState
+{
+ List *elemsList;
+ ListCell **elemCells;
+ void **elems;
+ int *positions;
+ int mutatorNColumns;
+ int count;
+} PathkeyMutatorState;
+
+
+/*
+ * PathkeyMutatorInit
+ * Initialize state of the permutation generator.
+ *
+ * We want to generate permutations of elements in the "elems" list. We may want
+ * to skip some number of elements at the beginning (when treating as presorted)
+ * or at the end (we only permute a limited number of group keys).
+ *
+ * The list is decomposed into elements, and we also keep pointers to individual
+ * cells. This allows us to build the permuted list quickly and cheaply, without
+ * creating any copies.
+ */
+static void
+PathkeyMutatorInit(PathkeyMutatorState *state, List *elems, int start, int end)
+{
+ int i;
+ int n = end - start;
+ ListCell *lc;
+
+ memset(state, 0, sizeof(*state));
+
+ state->mutatorNColumns = n;
+
+ state->elemsList = list_copy(elems);
+
+ state->elems = palloc(sizeof(void *) * n);
+ state->elemCells = palloc(sizeof(ListCell *) * n);
+ state->positions = palloc(sizeof(int) * n);
+
+ i = 0;
+ for_each_cell(lc, state->elemsList, list_nth_cell(state->elemsList, start))
+ {
+ state->elemCells[i] = lc;
+ state->elems[i] = lfirst(lc);
+ state->positions[i] = i + 1;
+ i++;
+
+ if (i >= n)
+ break;
+ }
+}
+
+/* Swap two elements of an array. */
+static void
+PathkeyMutatorSwap(int *a, int i, int j)
+{
+ int s = a[i];
+
+ a[i] = a[j];
+ a[j] = s;
+}
+
+/*
+ * Generate the next permutation of elements.
+ */
+static bool
+PathkeyMutatorNextSet(int *a, int n)
+{
+ int j,
+ k,
+ l,
+ r;
+
+ j = n - 2;
+
+ while (j >= 0 && a[j] >= a[j + 1])
+ j--;
+
+ if (j < 0)
+ return false;
+
+ k = n - 1;
+
+ while (k > 0 && a[j] >= a[k])
+ k--;
+
+ PathkeyMutatorSwap(a, j, k);
+
+ l = j + 1;
+ r = n - 1;
+
+ while (l < r)
+ PathkeyMutatorSwap(a, l++, r--);
+
+ return true;
+}
+
+/*
+ * PathkeyMutatorNext
+ * Generate the next permutation of list of elements.
+ *
+ * Returns the next permutation (as a list of elements) or NIL if there are no
+ * more permutations.
+ */
+static List *
+PathkeyMutatorNext(PathkeyMutatorState *state)
+{
+ int i;
+
+ state->count++;
+
+ /* first permutation is original list */
+ if (state->count == 1)
+ return state->elemsList;
+
+ /* when there are no more permutations, return NIL */
+ if (!PathkeyMutatorNextSet(state->positions, state->mutatorNColumns))
+ {
+ pfree(state->elems);
+ pfree(state->elemCells);
+ pfree(state->positions);
+
+ list_free(state->elemsList);
+
+ return NIL;
+ }
+
+ /* update the list cells to point to the right elements */
+ for (i = 0; i < state->mutatorNColumns; i++)
+ lfirst(state->elemCells[i]) =
+ (void *) state->elems[state->positions[i] - 1];
+
+ return state->elemsList;
+}
+
+/*
+ * Cost of comparing pathkeys.
+ */
+typedef struct PathkeySortCost
+{
+ Cost cost;
+ PathKey *pathkey;
+} PathkeySortCost;
+
+static int
+pathkey_sort_cost_comparator(const void *_a, const void *_b)
+{
+ const PathkeySortCost *a = (PathkeySortCost *) _a;
+ const PathkeySortCost *b = (PathkeySortCost *) _b;
+
+ if (a->cost < b->cost)
+ return -1;
+ else if (a->cost == b->cost)
+ return 0;
+ return 1;
+}
+
+/*
+ * get_cheapest_group_keys_order
+ * Reorders the group pathkeys / clauses to minimize the comparison cost.
+ *
+ * Given the list of pathkeys in '*group_pathkeys', we try to arrange these
+ * in an order that minimizes the sort costs that will be incurred by the
+ * GROUP BY. The costs mainly depend on the cost of the sort comparator
+ * function(s) and the number of distinct values in each column of the GROUP
+ * BY clause (*group_clauses). Sorting on subsequent columns is only required
+ * for tiebreak situations where two values sort equally.
+ *
+ * In case the input is partially sorted, only the remaining pathkeys are
+ * considered. 'n_preordered' denotes how many of the leading *group_pathkeys
+ * the input is presorted by.
+ *
+ * Returns true and sets *group_pathkeys and *group_clauses to the newly
+ * ordered versions of the lists that were passed in via these parameters.
+ * If no reordering was deemed necessary then we return false, in which case
+ * the *group_pathkeys and *group_clauses lists are left untouched. The
+ * original *group_pathkeys and *group_clauses parameter values are never
+ * destructively modified in place.
+ */
+static bool
+get_cheapest_group_keys_order(PlannerInfo *root, double nrows,
+ List **group_pathkeys, List **group_clauses,
+ int n_preordered)
+{
+ List *new_group_pathkeys = NIL;
+ List *new_group_clauses = NIL;
+ List *var_group_pathkeys;
+ ListCell *cell;
+ PathkeyMutatorState mstate;
+ double cheapest_sort_cost = DBL_MAX;
+ int nFreeKeys;
+ int nToPermute;
+
+ /* If there are less than 2 unsorted pathkeys, we're done. */
+ if (root->num_groupby_pathkeys - n_preordered < 2)
+ return false;
+
+ /*
+ * We could exhaustively cost all possible orderings of the pathkeys, but
+ * for a large number of pathkeys it might be prohibitively expensive. So
+ * we try to apply simple cheap heuristics first - we sort the pathkeys by
+ * sort cost (as if the pathkey was sorted independently) and then check
+ * only the four cheapest pathkeys. The remaining pathkeys are kept
+ * ordered by cost.
+ *
+ * XXX This is a very simple heuristics, but likely to work fine for most
+ * cases (because the number of GROUP BY clauses tends to be lower than
+ * 4). But it ignores how the number of distinct values in each pathkey
+ * affects the following steps. It might be better to use "more expensive"
+ * pathkey first if it has many distinct values, because it then limits
+ * the number of comparisons for the remaining pathkeys. But evaluating
+ * that is likely quite the expensive.
+ */
+ nFreeKeys = root->num_groupby_pathkeys - n_preordered;
+ nToPermute = 4;
+ if (nFreeKeys > nToPermute)
+ {
+ PathkeySortCost *costs = palloc(sizeof(PathkeySortCost) * nFreeKeys);
+ PathkeySortCost *cost = costs;
+ int i = 0;
+
+ /*
+ * Estimate cost for sorting individual pathkeys skipping the
+ * pre-ordered pathkeys.
+ */
+ for_each_from(cell, *group_pathkeys, n_preordered)
+ {
+ PathKey *pathkey = (PathKey *) lfirst(cell);
+ List *to_cost;
+
+ if (foreach_current_index(cell) >= root->num_groupby_pathkeys)
+ break;
+
+ i++;
+ to_cost = list_make1(pathkey);
+ cost->pathkey = pathkey;
+ cost->cost = cost_sort_estimate(root, to_cost, 0, nrows);
+ cost++;
+
+ list_free(to_cost);
+ }
+
+ Assert(i == nFreeKeys);
+ /* sort the pathkeys by sort cost in ascending order */
+ qsort(costs, nFreeKeys, sizeof(*costs), pathkey_sort_cost_comparator);
+
+ /*
+ * Rebuild the list of pathkeys - first the preordered ones, then the
+ * rest ordered by cost.
+ */
+ new_group_pathkeys = list_copy_head(*group_pathkeys, n_preordered);
+
+ for (int i = 0; i < nFreeKeys; i++)
+ new_group_pathkeys = lappend(new_group_pathkeys, costs[i].pathkey);
+
+ new_group_pathkeys = list_copy_tail(*group_pathkeys,
+ root->num_groupby_pathkeys);
+ pfree(costs);
+ }
+ else
+ {
+ /* Copy the list, so that we can free the new list by list_free. */
+ new_group_pathkeys = list_copy(*group_pathkeys);
+ nToPermute = nFreeKeys;
+ }
+
+ Assert(list_length(new_group_pathkeys) == list_length(*group_pathkeys));
+
+ /*
+ * Generate pathkey lists with permutations of the first nToPermute
+ * pathkeys.
+ *
+ * XXX We simply calculate sort cost for each individual pathkey list, but
+ * there's room for two dynamic programming optimizations here. Firstly,
+ * we may pass the current "best" cost to cost_sort_estimate so that it
+ * can "abort" if the estimated pathkeys list exceeds it. Secondly, it
+ * could pass the return information about the position when it exceeded
+ * the cost, and we could skip all permutations with the same prefix.
+ *
+ * Imagine we've already found ordering with cost C1, and we're evaluating
+ * another ordering - cost_sort_estimate() calculates cost by adding the
+ * pathkeys one by one (more or less), and the cost only grows. If at any
+ * point it exceeds C1, it can't possibly be "better" so we can discard
+ * it. But we also know that we can discard all ordering with the same
+ * prefix, because if we're estimating (a,b,c,d) and we exceed C1 at (a,b)
+ * then the same thing will happen for any ordering with this prefix.
+ */
+ PathkeyMutatorInit(&mstate, new_group_pathkeys, n_preordered,
+ n_preordered + nToPermute);
+
+ while ((var_group_pathkeys = PathkeyMutatorNext(&mstate)) != NIL)
+ {
+ Cost cost;
+
+ cost = cost_sort_estimate(root, var_group_pathkeys, n_preordered, nrows);
+
+ if (cost < cheapest_sort_cost)
+ {
+ list_free(new_group_pathkeys);
+ new_group_pathkeys = list_copy(var_group_pathkeys);
+ cheapest_sort_cost = cost;
+ }
+ }
+
+ Assert(list_length(new_group_pathkeys) == list_length(root->group_pathkeys));
+
+ /* Reorder the group clauses according to the reordered pathkeys. */
+ foreach(cell, new_group_pathkeys)
+ {
+ PathKey *pathkey = (PathKey *) lfirst(cell);
+
+ if (foreach_current_index(cell) >= root->num_groupby_pathkeys)
+ break;
+
+ new_group_clauses =
+ lappend(new_group_clauses,
+ get_sortgroupref_clause(pathkey->pk_eclass->ec_sortref,
+ *group_clauses));
+ }
+
+ /* Just append the rest GROUP BY clauses */
+ new_group_clauses = list_concat_unique_ptr(new_group_clauses,
+ *group_clauses);
+ *group_pathkeys = new_group_pathkeys;
+ *group_clauses = new_group_clauses;
+ Assert(list_length(new_group_clauses) == list_length(root->processed_groupClause));
+
+ return true;
+}
+
+/*
+ * get_useful_group_keys_orderings
+ * Determine which orderings of GROUP BY keys are potentially interesting.
+ *
+ * Returns list of PathKeyInfo items, each representing an interesting ordering
+ * of GROUP BY keys. Each item stores pathkeys and clauses in matching order.
+ *
+ * The function considers (and keeps) multiple group by orderings:
+ *
+ * - the original ordering, as specified by the GROUP BY clause
+ *
+ * - GROUP BY keys reordered to minimize the sort cost
+ *
+ * - GROUP BY keys reordered to match path ordering (as much as possible), with
+ * the tail reordered to minimize the sort cost
+ *
+ * - GROUP BY keys to match target ORDER BY clause (as much as possible), with
+ * the tail reordered to minimize the sort cost
+ *
+ * There are other potentially interesting orderings (e.g. it might be best to
+ * match the first ORDER BY key, order the remaining keys differently and then
+ * rely on the incremental sort to fix this), but we ignore those for now. To
+ * make this work we'd have to pretty much generate all possible permutations.
+ */
+List *
+get_useful_group_keys_orderings(PlannerInfo *root, double nrows,
+ List *path_pathkeys)
+{
+ Query *parse = root->parse;
+ List *infos = NIL;
+ PathKeyInfo *info;
+ int n_preordered = 0;
+
+ List *pathkeys = root->group_pathkeys;
+ List *clauses = root->processed_groupClause;
+
+ /* always return at least the original pathkeys/clauses */
+ info = makeNode(PathKeyInfo);
+ info->pathkeys = pathkeys;
+ info->clauses = clauses;
+ infos = lappend(infos, info);
+
+ /*
+ * Should we try generating alternative orderings of the group keys? If
+ * not, we produce only the order specified in the query, i.e. the
+ * optimization is effectively disabled.
+ */
+ if (!enable_group_by_reordering)
+ return infos;
+
+ /* for grouping sets we can't do any reordering */
+ if (parse->groupingSets)
+ return infos;
+
+ /*
+ * Try reordering pathkeys to minimize the sort cost, ignoring both the
+ * target ordering (ORDER BY) and ordering of the input path.
+ */
+ if (get_cheapest_group_keys_order(root, nrows, &pathkeys, &clauses,
+ n_preordered))
+ {
+ Assert(list_length(pathkeys) == list_length(clauses));
+
+ info = makeNode(PathKeyInfo);
+ info->pathkeys = pathkeys;
+ info->clauses = clauses;
+
+ infos = lappend(infos, info);
+ }
+
+ /*
+ * If the path is sorted in some way, try reordering the group keys to
+ * match as much of the ordering as possible - we get this sort for free
+ * (mostly).
+ *
+ * We must not do this when there are no grouping sets, because those use
+ * more complex logic to decide the ordering.
+ *
+ * XXX Isn't this somewhat redundant with presorted_keys? Actually, it's
+ * more a complement, because it allows benefiting from incremental sort
+ * as much as possible.
+ *
+ * XXX This does nothing if (n_preordered == 0). We shouldn't create the
+ * info in this case.
+ */
+ if (path_pathkeys)
+ {
+ n_preordered = group_keys_reorder_by_pathkeys(path_pathkeys,
+ &pathkeys,
+ &clauses,
+ root->num_groupby_pathkeys);
+
+ /* reorder the tail to minimize sort cost */
+ get_cheapest_group_keys_order(root, nrows, &pathkeys, &clauses,
+ n_preordered);
+
+ /*
+ * reorder the tail to minimize sort cost
+ *
+ * XXX Ignore the return value - there may be nothing to reorder, in
+ * which case get_cheapest_group_keys_order returns false. But we
+ * still want to keep the keys reordered to path_pathkeys.
+ */
+ info = makeNode(PathKeyInfo);
+ info->pathkeys = pathkeys;
+ info->clauses = clauses;
+
+ infos = lappend(infos, info);
+ }
+
+ /*
+ * Try reordering pathkeys to minimize the sort cost (this time consider
+ * the ORDER BY clause, but only if set debug_group_by_match_order_by).
+ */
+ if (root->sort_pathkeys)
+ {
+ n_preordered = group_keys_reorder_by_pathkeys(root->sort_pathkeys,
+ &pathkeys,
+ &clauses,
+ root->num_groupby_pathkeys);
+
+ /*
+ * reorder the tail to minimize sort cost
+ *
+ * XXX Ignore the return value - there may be nothing to reorder, in
+ * which case get_cheapest_group_keys_order returns false. But we
+ * still want to keep the keys reordered to sort_pathkeys.
+ */
+ get_cheapest_group_keys_order(root, nrows, &pathkeys, &clauses,
+ n_preordered);
+
+ /* keep the group keys reordered to match ordering of input path */
+ info = makeNode(PathKeyInfo);
+ info->pathkeys = pathkeys;
+ info->clauses = clauses;
+
+ infos = lappend(infos, info);
+ }
+
+ return infos;
+}
+
/*
* pathkeys_count_contained_in
* Same as pathkeys_contained_in, but also sets length of longest
@@ -1939,6 +2488,54 @@ pathkeys_useful_for_ordering(PlannerInfo *root, List *pathkeys)
return n_common_pathkeys;
}
+/*
+ * pathkeys_useful_for_grouping
+ * Count the number of pathkeys that are useful for grouping (instead of
+ * explicit sort)
+ *
+ * Group pathkeys could be reordered to benefit from the ordering. The
+ * ordering may not be "complete" and may require incremental sort, but that's
+ * fine. So we simply count prefix pathkeys with a matching group key, and
+ * stop once we find the first pathkey without a match.
+ *
+ * So e.g. with pathkeys (a,b,c) and group keys (a,b,e) this determines (a,b)
+ * pathkeys are useful for grouping, and we might do incremental sort to get
+ * path ordered by (a,b,e).
+ *
+ * This logic is necessary to retain paths with ordering not matching grouping
+ * keys directly, without the reordering.
+ *
+ * Returns the length of pathkey prefix with matching group keys.
+ */
+static int
+pathkeys_useful_for_grouping(PlannerInfo *root, List *pathkeys)
+{
+ ListCell *key;
+ int n = 0;
+
+ /* no special ordering requested for grouping */
+ if (root->group_pathkeys == NIL)
+ return 0;
+
+ /* unordered path */
+ if (pathkeys == NIL)
+ return 0;
+
+ /* walk the pathkeys and search for matching group key */
+ foreach(key, pathkeys)
+ {
+ PathKey *pathkey = (PathKey *) lfirst(key);
+
+ /* no matching group key, we're done */
+ if (!list_member_ptr(root->group_pathkeys, pathkey))
+ break;
+
+ n++;
+ }
+
+ return n;
+}
+
/*
* truncate_useless_pathkeys
* Shorten the given pathkey list to just the useful pathkeys.
@@ -1953,6 +2550,9 @@ truncate_useless_pathkeys(PlannerInfo *root,
nuseful = pathkeys_useful_for_merging(root, rel, pathkeys);
nuseful2 = pathkeys_useful_for_ordering(root, pathkeys);
+ if (nuseful2 > nuseful)
+ nuseful = nuseful2;
+ nuseful2 = pathkeys_useful_for_grouping(root, pathkeys);
if (nuseful2 > nuseful)
nuseful = nuseful2;
@@ -1988,6 +2588,8 @@ has_useful_pathkeys(PlannerInfo *root, RelOptInfo *rel)
{
if (rel->joininfo != NIL || rel->has_eclass_joins)
return true; /* might be able to use pathkeys for merging */
+ if (root->group_pathkeys != NIL)
+ return true; /* might be able to use pathkeys for grouping */
if (root->query_pathkeys != NIL)
return true; /* might be able to use them for ordering */
return false; /* definitely useless */
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index 44efb1f4eb..4a7c9d9440 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -2810,8 +2810,9 @@ remove_useless_groupby_columns(PlannerInfo *root)
*
* In principle it might be interesting to consider other orderings of the
* GROUP BY elements, which could match the sort ordering of other
- * possible plans (eg an indexscan) and thereby reduce cost. We don't
- * bother with that, though. Hashed grouping will frequently win anyway.
+ * possible plans (eg an indexscan) and thereby reduce cost. However, we
+ * don't yet have sufficient information to do that here, so that's left until
+ * later in planning. See get_useful_group_keys_orderings().
*
* Note: we need no comparable processing of the distinctClause because
* the parser already enforced that that matches ORDER BY.
@@ -3468,6 +3469,7 @@ standard_qp_callback(PlannerInfo *root, void *extra)
tlist,
true,
&sortable);
+
if (!sortable)
{
/* Can't sort; no point in considering aggregate ordering either */
@@ -6842,90 +6844,108 @@ add_paths_to_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel,
*/
foreach(lc, input_rel->pathlist)
{
+ ListCell *lc2;
Path *path = (Path *) lfirst(lc);
- bool is_sorted;
- int presorted_keys;
+ Path *path_save = path;
+ List *pathkey_orderings = NIL;
- is_sorted = pathkeys_count_contained_in(root->group_pathkeys,
- path->pathkeys,
- &presorted_keys);
+ /* generate alternative group orderings that might be useful */
+ pathkey_orderings = get_useful_group_keys_orderings(root,
+ path->rows,
+ path->pathkeys);
+
+ Assert(list_length(pathkey_orderings) > 0);
- if (!is_sorted)
+ foreach(lc2, pathkey_orderings)
{
- /*
- * Try at least sorting the cheapest path and also try
- * incrementally sorting any path which is partially sorted
- * already (no need to deal with paths which have presorted
- * keys when incremental sort is disabled unless it's the
- * cheapest input path).
- */
- if (path != cheapest_path &&
- (presorted_keys == 0 || !enable_incremental_sort))
- continue;
+ bool is_sorted;
+ int presorted_keys;
+ PathKeyInfo *info = (PathKeyInfo *) lfirst(lc2);
- /*
- * We've no need to consider both a sort and incremental sort.
- * We'll just do a sort if there are no presorted keys and an
- * incremental sort when there are presorted keys.
- */
- if (presorted_keys == 0 || !enable_incremental_sort)
- path = (Path *) create_sort_path(root,
- grouped_rel,
- path,
- root->group_pathkeys,
- -1.0);
- else
- path = (Path *) create_incremental_sort_path(root,
- grouped_rel,
- path,
- root->group_pathkeys,
- presorted_keys,
- -1.0);
- }
+ /* restore the path (we replace it in the loop) */
+ path = path_save;
- /* Now decide what to stick atop it */
- if (parse->groupingSets)
- {
- consider_groupingsets_paths(root, grouped_rel,
- path, true, can_hash,
- gd, agg_costs, dNumGroups);
- }
- else if (parse->hasAggs)
- {
- /*
- * We have aggregation, possibly with plain GROUP BY. Make an
- * AggPath.
- */
- add_path(grouped_rel, (Path *)
+ is_sorted = pathkeys_count_contained_in(info->pathkeys,
+ path->pathkeys,
+ &presorted_keys);
+
+ if (!is_sorted)
+ {
+ /*
+ * Try at least sorting the cheapest path and also try
+ * incrementally sorting any path which is partially sorted
+ * already (no need to deal with paths which have presorted
+ * keys when incremental sort is disabled unless it's the
+ * cheapest input path).
+ */
+ if (path != cheapest_path &&
+ (presorted_keys == 0 || !enable_incremental_sort))
+ continue;
+
+ /*
+ * We've no need to consider both a sort and incremental sort.
+ * We'll just do a sort if there are no presorted keys and an
+ * incremental sort when there are presorted keys.
+ */
+ if (presorted_keys == 0 || !enable_incremental_sort)
+ path = (Path *) create_sort_path(root,
+ grouped_rel,
+ path,
+ info->pathkeys,
+ -1.0);
+ else
+ path = (Path *) create_incremental_sort_path(root,
+ grouped_rel,
+ path,
+ info->pathkeys,
+ presorted_keys,
+ -1.0);
+ }
+
+ /* Now decide what to stick atop it */
+ if (parse->groupingSets)
+ {
+ consider_groupingsets_paths(root, grouped_rel,
+ path, true, can_hash,
+ gd, agg_costs, dNumGroups);
+ }
+ else if (parse->hasAggs)
+ {
+ /*
+ * We have aggregation, possibly with plain GROUP BY. Make an
+ * AggPath.
+ */
+ add_path(grouped_rel, (Path *)
create_agg_path(root,
grouped_rel,
path,
grouped_rel->reltarget,
parse->groupClause ? AGG_SORTED : AGG_PLAIN,
AGGSPLIT_SIMPLE,
- root->processed_groupClause,
+ info->clauses,
havingQual,
agg_costs,
dNumGroups));
- }
- else if (parse->groupClause)
- {
- /*
- * We have GROUP BY without aggregation or grouping sets. Make
- * a GroupPath.
- */
- add_path(grouped_rel, (Path *)
+ }
+ else if (parse->groupClause)
+ {
+ /*
+ * We have GROUP BY without aggregation or grouping sets. Make
+ * a GroupPath.
+ */
+ add_path(grouped_rel, (Path *)
create_group_path(root,
grouped_rel,
path,
- root->processed_groupClause,
+ info->clauses,
havingQual,
dNumGroups));
- }
- else
- {
- /* Other cases should have been handled above */
- Assert(false);
+ }
+ else
+ {
+ /* Other cases should have been handled above */
+ Assert(false);
+ }
}
}
@@ -6937,69 +6957,84 @@ add_paths_to_grouping_rel(PlannerInfo *root, RelOptInfo *input_rel,
{
foreach(lc, partially_grouped_rel->pathlist)
{
+ ListCell *lc2;
Path *path = (Path *) lfirst(lc);
- bool is_sorted;
- int presorted_keys;
+ List *pathkey_orderings = NIL;
- is_sorted = pathkeys_count_contained_in(root->group_pathkeys,
- path->pathkeys,
- &presorted_keys);
+ /* generate alternative group orderings that might be useful */
+ pathkey_orderings = get_useful_group_keys_orderings(root,
+ path->rows,
+ path->pathkeys);
- if (!is_sorted)
+ Assert(list_length(pathkey_orderings) > 0);
+
+ /* process all potentially interesting grouping reorderings */
+ foreach(lc2, pathkey_orderings)
{
- /*
- * Try at least sorting the cheapest path and also try
- * incrementally sorting any path which is partially
- * sorted already (no need to deal with paths which have
- * presorted keys when incremental sort is disabled unless
- * it's the cheapest input path).
- */
- if (path != partially_grouped_rel->cheapest_total_path &&
- (presorted_keys == 0 || !enable_incremental_sort))
- continue;
+ bool is_sorted;
+ int presorted_keys;
+ PathKeyInfo *info = (PathKeyInfo *) lfirst(lc2);
- /*
- * We've no need to consider both a sort and incremental
- * sort. We'll just do a sort if there are no pre-sorted
- * keys and an incremental sort when there are presorted
- * keys.
- */
- if (presorted_keys == 0 || !enable_incremental_sort)
- path = (Path *) create_sort_path(root,
- grouped_rel,
- path,
- root->group_pathkeys,
- -1.0);
- else
- path = (Path *) create_incremental_sort_path(root,
- grouped_rel,
- path,
- root->group_pathkeys,
- presorted_keys,
- -1.0);
- }
+ is_sorted = pathkeys_count_contained_in(root->group_pathkeys,
+ path->pathkeys,
+ &presorted_keys);
- if (parse->hasAggs)
- add_path(grouped_rel, (Path *)
+ if (!is_sorted)
+ {
+ /*
+ * Try at least sorting the cheapest path and also try
+ * incrementally sorting any path which is partially
+ * sorted already (no need to deal with paths which have
+ * presorted keys when incremental sort is disabled unless
+ * it's the cheapest input path).
+ */
+ if (path != partially_grouped_rel->cheapest_total_path &&
+ (presorted_keys == 0 || !enable_incremental_sort))
+ continue;
+
+ /*
+ * We've no need to consider both a sort and incremental
+ * sort. We'll just do a sort if there are no pre-sorted
+ * keys and an incremental sort when there are presorted
+ * keys.
+ */
+ if (presorted_keys == 0 || !enable_incremental_sort)
+ path = (Path *) create_sort_path(root,
+ grouped_rel,
+ path,
+ info->pathkeys,
+ -1.0);
+ else
+ path = (Path *) create_incremental_sort_path(root,
+ grouped_rel,
+ path,
+ info->pathkeys,
+ presorted_keys,
+ -1.0);
+ }
+
+ if (parse->hasAggs)
+ add_path(grouped_rel, (Path *)
create_agg_path(root,
grouped_rel,
path,
grouped_rel->reltarget,
parse->groupClause ? AGG_SORTED : AGG_PLAIN,
AGGSPLIT_FINAL_DESERIAL,
- root->processed_groupClause,
+ info->clauses,
havingQual,
agg_final_costs,
dNumGroups));
- else
- add_path(grouped_rel, (Path *)
+ else
+ add_path(grouped_rel, (Path *)
create_group_path(root,
grouped_rel,
path,
- root->processed_groupClause,
+ info->clauses,
havingQual,
dNumGroups));
+ }
}
}
}
@@ -7202,66 +7237,85 @@ create_partial_grouping_paths(PlannerInfo *root,
*/
foreach(lc, input_rel->pathlist)
{
+ ListCell *lc2;
Path *path = (Path *) lfirst(lc);
- bool is_sorted;
- int presorted_keys;
+ Path *path_save = path;
+ List *pathkey_orderings = NIL;
- is_sorted = pathkeys_count_contained_in(root->group_pathkeys,
- path->pathkeys,
- &presorted_keys);
- if (!is_sorted)
+ /* generate alternative group orderings that might be useful */
+ pathkey_orderings = get_useful_group_keys_orderings(root,
+ path->rows,
+ path->pathkeys);
+
+ Assert(list_length(pathkey_orderings) > 0);
+
+ /* process all potentially interesting grouping reorderings */
+ foreach(lc2, pathkey_orderings)
{
- /*
- * Try at least sorting the cheapest path and also try
- * incrementally sorting any path which is partially sorted
- * already (no need to deal with paths which have presorted
- * keys when incremental sort is disabled unless it's the
- * cheapest input path).
- */
- if (path != cheapest_total_path &&
- (presorted_keys == 0 || !enable_incremental_sort))
- continue;
+ bool is_sorted;
+ int presorted_keys;
+ PathKeyInfo *info = (PathKeyInfo *) lfirst(lc2);
- /*
- * We've no need to consider both a sort and incremental sort.
- * We'll just do a sort if there are no presorted keys and an
- * incremental sort when there are presorted keys.
- */
- if (presorted_keys == 0 || !enable_incremental_sort)
- path = (Path *) create_sort_path(root,
- partially_grouped_rel,
- path,
- root->group_pathkeys,
- -1.0);
- else
- path = (Path *) create_incremental_sort_path(root,
- partially_grouped_rel,
- path,
- root->group_pathkeys,
- presorted_keys,
- -1.0);
- }
+ /* restore the path (we replace it in the loop) */
+ path = path_save;
- if (parse->hasAggs)
- add_path(partially_grouped_rel, (Path *)
+ is_sorted = pathkeys_count_contained_in(info->pathkeys,
+ path->pathkeys,
+ &presorted_keys);
+ if (!is_sorted)
+ {
+ /*
+ * Try at least sorting the cheapest path and also try
+ * incrementally sorting any path which is partially sorted
+ * already (no need to deal with paths which have presorted
+ * keys when incremental sort is disabled unless it's the
+ * cheapest input path).
+ */
+ if (path != cheapest_total_path &&
+ (presorted_keys == 0 || !enable_incremental_sort))
+ continue;
+
+ /*
+ * We've no need to consider both a sort and incremental sort.
+ * We'll just do a sort if there are no presorted keys and an
+ * incremental sort when there are presorted keys.
+ */
+ if (presorted_keys == 0 || !enable_incremental_sort)
+ path = (Path *) create_sort_path(root,
+ partially_grouped_rel,
+ path,
+ info->pathkeys,
+ -1.0);
+ else
+ path = (Path *) create_incremental_sort_path(root,
+ partially_grouped_rel,
+ path,
+ info->pathkeys,
+ presorted_keys,
+ -1.0);
+ }
+
+ if (parse->hasAggs)
+ add_path(partially_grouped_rel, (Path *)
create_agg_path(root,
partially_grouped_rel,
path,
partially_grouped_rel->reltarget,
parse->groupClause ? AGG_SORTED : AGG_PLAIN,
AGGSPLIT_INITIAL_SERIAL,
- root->processed_groupClause,
+ info->clauses,
NIL,
agg_partial_costs,
dNumPartialGroups));
- else
- add_path(partially_grouped_rel, (Path *)
- create_group_path(root,
- partially_grouped_rel,
- path,
- root->processed_groupClause,
- NIL,
- dNumPartialGroups));
+ else
+ add_path(partially_grouped_rel, (Path *)
+ create_group_path(root,
+ partially_grouped_rel,
+ path,
+ info->clauses,
+ NIL,
+ dNumPartialGroups));
+ }
}
}
@@ -7270,67 +7324,86 @@ create_partial_grouping_paths(PlannerInfo *root,
/* Similar to above logic, but for partial paths. */
foreach(lc, input_rel->partial_pathlist)
{
+ ListCell *lc2;
Path *path = (Path *) lfirst(lc);
- bool is_sorted;
- int presorted_keys;
+ Path *path_save = path;
+ List *pathkey_orderings = NIL;
- is_sorted = pathkeys_count_contained_in(root->group_pathkeys,
- path->pathkeys,
- &presorted_keys);
+ /* generate alternative group orderings that might be useful */
+ pathkey_orderings = get_useful_group_keys_orderings(root,
+ path->rows,
+ path->pathkeys);
+
+ Assert(list_length(pathkey_orderings) > 0);
- if (!is_sorted)
+ /* process all potentially interesting grouping reorderings */
+ foreach(lc2, pathkey_orderings)
{
- /*
- * Try at least sorting the cheapest path and also try
- * incrementally sorting any path which is partially sorted
- * already (no need to deal with paths which have presorted
- * keys when incremental sort is disabled unless it's the
- * cheapest input path).
- */
- if (path != cheapest_partial_path &&
- (presorted_keys == 0 || !enable_incremental_sort))
- continue;
+ bool is_sorted;
+ int presorted_keys;
+ PathKeyInfo *info = (PathKeyInfo *) lfirst(lc2);
- /*
- * We've no need to consider both a sort and incremental sort.
- * We'll just do a sort if there are no presorted keys and an
- * incremental sort when there are presorted keys.
- */
- if (presorted_keys == 0 || !enable_incremental_sort)
- path = (Path *) create_sort_path(root,
- partially_grouped_rel,
- path,
- root->group_pathkeys,
- -1.0);
- else
- path = (Path *) create_incremental_sort_path(root,
- partially_grouped_rel,
- path,
- root->group_pathkeys,
- presorted_keys,
- -1.0);
- }
+ /* restore the path (we replace it in the loop) */
+ path = path_save;
- if (parse->hasAggs)
- add_partial_path(partially_grouped_rel, (Path *)
+ is_sorted = pathkeys_count_contained_in(info->pathkeys,
+ path->pathkeys,
+ &presorted_keys);
+
+ if (!is_sorted)
+ {
+ /*
+ * Try at least sorting the cheapest path and also try
+ * incrementally sorting any path which is partially sorted
+ * already (no need to deal with paths which have presorted
+ * keys when incremental sort is disabled unless it's the
+ * cheapest input path).
+ */
+ if (path != cheapest_partial_path &&
+ (presorted_keys == 0 || !enable_incremental_sort))
+ continue;
+
+ /*
+ * We've no need to consider both a sort and incremental sort.
+ * We'll just do a sort if there are no presorted keys and an
+ * incremental sort when there are presorted keys.
+ */
+ if (presorted_keys == 0 || !enable_incremental_sort)
+ path = (Path *) create_sort_path(root,
+ partially_grouped_rel,
+ path,
+ info->pathkeys,
+ -1.0);
+ else
+ path = (Path *) create_incremental_sort_path(root,
+ partially_grouped_rel,
+ path,
+ info->pathkeys,
+ presorted_keys,
+ -1.0);
+ }
+
+ if (parse->hasAggs)
+ add_partial_path(partially_grouped_rel, (Path *)
create_agg_path(root,
partially_grouped_rel,
path,
partially_grouped_rel->reltarget,
parse->groupClause ? AGG_SORTED : AGG_PLAIN,
AGGSPLIT_INITIAL_SERIAL,
- root->processed_groupClause,
+ info->clauses,
NIL,
agg_partial_costs,
dNumPartialPartialGroups));
- else
- add_partial_path(partially_grouped_rel, (Path *)
+ else
+ add_partial_path(partially_grouped_rel, (Path *)
create_group_path(root,
partially_grouped_rel,
path,
- root->processed_groupClause,
+ info->clauses,
NIL,
dNumPartialPartialGroups));
+ }
}
}
@@ -7444,6 +7517,8 @@ gather_grouping_paths(PlannerInfo *root, RelOptInfo *rel)
* We can also skip the entire loop when we only have a single-item
* group_pathkeys because then we can't possibly have a presorted prefix
* of the list without having the list be fully sorted.
+ *
+ * XXX Shouldn't this also consider the group-key-reordering?
*/
if (!enable_incremental_sort || list_length(root->group_pathkeys) == 1)
return;
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 211ba65389..7b8d0cd294 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -1346,12 +1346,12 @@ create_append_path(PlannerInfo *root,
pathnode->path.total_cost = child->total_cost;
}
else
- cost_append(pathnode);
+ cost_append(pathnode, root);
/* Must do this last, else cost_append complains */
pathnode->path.pathkeys = child->pathkeys;
}
else
- cost_append(pathnode);
+ cost_append(pathnode, root);
/* If the caller provided a row estimate, override the computed value. */
if (rows >= 0)
diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c
index c4fcd0076e..cb7e67099e 100644
--- a/src/backend/utils/adt/selfuncs.c
+++ b/src/backend/utils/adt/selfuncs.c
@@ -3386,11 +3386,28 @@ double
estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
List **pgset, EstimationInfo *estinfo)
{
- List *varinfos = NIL;
+ return estimate_num_groups_incremental(root, groupExprs,
+ input_rows, pgset, estinfo,
+ NULL, 0);
+}
+
+/*
+ * estimate_num_groups_incremental
+ * An estimate_num_groups variant, optimized for cases that are adding the
+ * expressions incrementally (e.g. one by one).
+ */
+double
+estimate_num_groups_incremental(PlannerInfo *root, List *groupExprs,
+ double input_rows,
+ List **pgset, EstimationInfo *estinfo,
+ List **cache_varinfos, int prevNExprs)
+{
+ List *varinfos = (cache_varinfos) ? *cache_varinfos : NIL;
double srf_multiplier = 1.0;
double numdistinct;
ListCell *l;
- int i;
+ int i,
+ j;
/* Zero the estinfo output parameter, if non-NULL */
if (estinfo != NULL)
@@ -3421,7 +3438,7 @@ estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
*/
numdistinct = 1.0;
- i = 0;
+ i = j = 0;
foreach(l, groupExprs)
{
Node *groupexpr = (Node *) lfirst(l);
@@ -3430,6 +3447,14 @@ estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
List *varshere;
ListCell *l2;
+ /* was done on previous call */
+ if (cache_varinfos && j++ < prevNExprs)
+ {
+ if (pgset)
+ i++; /* to keep in sync with lines below */
+ continue;
+ }
+
/* is expression in this grouping set? */
if (pgset && !list_member_int(*pgset, i++))
continue;
@@ -3499,7 +3524,11 @@ estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
if (varshere == NIL)
{
if (contain_volatile_functions(groupexpr))
+ {
+ if (cache_varinfos)
+ *cache_varinfos = varinfos;
return input_rows;
+ }
continue;
}
@@ -3516,6 +3545,9 @@ estimate_num_groups(PlannerInfo *root, List *groupExprs, double input_rows,
}
}
+ if (cache_varinfos)
+ *cache_varinfos = varinfos;
+
/*
* If now no Vars, we must have an all-constant or all-boolean GROUP BY
* list.
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index bdb26e2b77..385f160ff8 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -1026,6 +1026,16 @@ struct config_bool ConfigureNamesBool[] =
true,
NULL, NULL, NULL
},
+ {
+ {"enable_group_by_reordering", PGC_USERSET, QUERY_TUNING_METHOD,
+ gettext_noop("Enables reordering of GROUP BY keys."),
+ NULL,
+ GUC_EXPLAIN
+ },
+ &enable_group_by_reordering,
+ true,
+ NULL, NULL, NULL
+ },
{
{"geqo", PGC_USERSET, QUERY_TUNING_GEQO,
gettext_noop("Enables genetic query optimization."),
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index 6bb39d39ba..751511745a 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -391,6 +391,7 @@
#enable_seqscan = on
#enable_sort = on
#enable_tidscan = on
+#enable_group_by_reordering = on
# - Planner Cost Constants -
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 5702fbba60..15ba2f8218 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -1456,6 +1456,16 @@ typedef struct PathKey
bool pk_nulls_first; /* do NULLs come before normal values? */
} PathKey;
+/*
+ * Combines information about pathkeys and the associated clauses.
+ */
+typedef struct PathKeyInfo
+{
+ NodeTag type;
+ List *pathkeys;
+ List *clauses;
+} PathKeyInfo;
+
/*
* VolatileFunctionStatus -- allows nodes to cache their
* contain_volatile_functions properties. VOLATILITY_UNKNOWN means not yet
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index bee090ffc2..bbb1132473 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -116,7 +116,9 @@ extern void cost_incremental_sort(Path *path,
Cost input_startup_cost, Cost input_total_cost,
double input_tuples, int width, Cost comparison_cost, int sort_mem,
double limit_tuples);
-extern void cost_append(AppendPath *apath);
+extern Cost cost_sort_estimate(PlannerInfo *root, List *pathkeys,
+ int nPresortedKeys, double tuples);
+extern void cost_append(AppendPath *path, PlannerInfo *root);
extern void cost_merge_append(Path *path, PlannerInfo *root,
List *pathkeys, int n_streams,
Cost input_startup_cost, Cost input_total_cost,
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index 50bc3b503a..e4ff16d0c4 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -24,6 +24,7 @@ extern PGDLLIMPORT bool enable_geqo;
extern PGDLLIMPORT int geqo_threshold;
extern PGDLLIMPORT int min_parallel_table_scan_size;
extern PGDLLIMPORT int min_parallel_index_scan_size;
+extern PGDLLIMPORT bool enable_group_by_reordering;
/* Hook for plugins to get control in set_rel_pathlist() */
typedef void (*set_rel_pathlist_hook_type) (PlannerInfo *root,
@@ -205,6 +206,12 @@ typedef enum
extern PathKeysComparison compare_pathkeys(List *keys1, List *keys2);
extern bool pathkeys_contained_in(List *keys1, List *keys2);
extern bool pathkeys_count_contained_in(List *keys1, List *keys2, int *n_common);
+extern int group_keys_reorder_by_pathkeys(List *pathkeys,
+ List **group_pathkeys,
+ List **group_clauses,
+ int num_groupby_pathkeys);
+extern List *get_useful_group_keys_orderings(PlannerInfo *root, double nrows,
+ List *path_pathkeys);
extern Path *get_cheapest_path_for_pathkeys(List *paths, List *pathkeys,
Relids required_outer,
CostSelector cost_criterion,
diff --git a/src/include/utils/selfuncs.h b/src/include/utils/selfuncs.h
index 2f76c473db..2b21c06cc9 100644
--- a/src/include/utils/selfuncs.h
+++ b/src/include/utils/selfuncs.h
@@ -214,6 +214,11 @@ extern double estimate_num_groups(PlannerInfo *root, List *groupExprs,
double input_rows, List **pgset,
EstimationInfo *estinfo);
+extern double estimate_num_groups_incremental(PlannerInfo *root, List *groupExprs,
+ double input_rows, List **pgset,
+ EstimationInfo *estinfo,
+ List **cache_varinfos, int prevNExprs);
+
extern void estimate_hash_bucket_stats(PlannerInfo *root,
Node *hashkey, double nbuckets,
Selectivity *mcv_freq,
diff --git a/src/test/regress/expected/aggregates.out b/src/test/regress/expected/aggregates.out
index d8271da4d1..205be28b3d 100644
--- a/src/test/regress/expected/aggregates.out
+++ b/src/test/regress/expected/aggregates.out
@@ -1234,7 +1234,8 @@ explain (costs off)
select distinct min(f1), max(f1) from minmaxtest;
QUERY PLAN
---------------------------------------------------------------------------------------------
- Unique
+ HashAggregate
+ Group Key: $0, $1
InitPlan 1 (returns $0)
-> Limit
-> Merge Append
@@ -1257,10 +1258,8 @@ explain (costs off)
-> Index Only Scan using minmaxtest2i on minmaxtest2 minmaxtest_8
Index Cond: (f1 IS NOT NULL)
-> Index Only Scan Backward using minmaxtest3i on minmaxtest3 minmaxtest_9
- -> Sort
- Sort Key: ($0), ($1)
- -> Result
-(26 rows)
+ -> Result
+(25 rows)
select distinct min(f1), max(f1) from minmaxtest;
min | max
@@ -2715,6 +2714,241 @@ SELECT balk(hundred) FROM tenk1;
(1 row)
ROLLBACK;
+-- GROUP BY optimization by reorder columns
+SELECT
+ i AS id,
+ i/2 AS p,
+ format('%60s', i%2) AS v,
+ i/4 AS c,
+ i/8 AS d,
+ (random() * (10000/8))::int as e --the same as d but no correlation with p
+ INTO btg
+FROM
+ generate_series(1, 10000) i;
+VACUUM btg;
+ANALYZE btg;
+-- GROUP BY optimization by reorder columns by frequency
+SET enable_hashagg=off;
+SET max_parallel_workers= 0;
+SET max_parallel_workers_per_gather = 0;
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, v;
+ QUERY PLAN
+-----------------------------
+ GroupAggregate
+ Group Key: p, v
+ -> Sort
+ Sort Key: p, v
+ -> Seq Scan on btg
+(5 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p;
+ QUERY PLAN
+-----------------------------
+ GroupAggregate
+ Group Key: p, v
+ -> Sort
+ Sort Key: p, v
+ -> Seq Scan on btg
+(5 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, c;
+ QUERY PLAN
+-----------------------------
+ GroupAggregate
+ Group Key: p, c, v
+ -> Sort
+ Sort Key: p, c, v
+ -> Seq Scan on btg
+(5 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, c ORDER BY v, p, c;
+ QUERY PLAN
+-----------------------------
+ GroupAggregate
+ Group Key: v, p, c
+ -> Sort
+ Sort Key: v, p, c
+ -> Seq Scan on btg
+(5 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, d, c;
+ QUERY PLAN
+------------------------------
+ GroupAggregate
+ Group Key: p, d, v, c
+ -> Sort
+ Sort Key: p, d, v, c
+ -> Seq Scan on btg
+(5 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, d, c ORDER BY v, p, d ,c;
+ QUERY PLAN
+------------------------------
+ GroupAggregate
+ Group Key: v, p, d, c
+ -> Sort
+ Sort Key: v, p, d, c
+ -> Seq Scan on btg
+(5 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, d, c ORDER BY p, v, d ,c;
+ QUERY PLAN
+------------------------------
+ GroupAggregate
+ Group Key: p, v, d, c
+ -> Sort
+ Sort Key: p, v, d, c
+ -> Seq Scan on btg
+(5 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, d, e;
+ QUERY PLAN
+-----------------------------
+ GroupAggregate
+ Group Key: p, d, e
+ -> Sort
+ Sort Key: p, d, e
+ -> Seq Scan on btg
+(5 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, e, d;
+ QUERY PLAN
+-----------------------------
+ GroupAggregate
+ Group Key: p, e, d
+ -> Sort
+ Sort Key: p, e, d
+ -> Seq Scan on btg
+(5 rows)
+
+CREATE STATISTICS btg_dep ON d, e, p FROM btg;
+ANALYZE btg;
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, d, e;
+ QUERY PLAN
+-----------------------------
+ GroupAggregate
+ Group Key: p, d, e
+ -> Sort
+ Sort Key: p, d, e
+ -> Seq Scan on btg
+(5 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, e, d;
+ QUERY PLAN
+-----------------------------
+ GroupAggregate
+ Group Key: p, e, d
+ -> Sort
+ Sort Key: p, e, d
+ -> Seq Scan on btg
+(5 rows)
+
+-- GROUP BY optimization by reorder columns by index scan
+CREATE INDEX ON btg(p, v);
+SET enable_seqscan=off;
+SET enable_bitmapscan=off;
+VACUUM btg;
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, v;
+ QUERY PLAN
+------------------------------------------------
+ GroupAggregate
+ Group Key: p, v
+ -> Index Only Scan using btg_p_v_idx on btg
+(3 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, v ORDER BY p, v;
+ QUERY PLAN
+------------------------------------------------
+ GroupAggregate
+ Group Key: p, v
+ -> Index Only Scan using btg_p_v_idx on btg
+(3 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p;
+ QUERY PLAN
+------------------------------------------------
+ GroupAggregate
+ Group Key: p, v
+ -> Index Only Scan using btg_p_v_idx on btg
+(3 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p ORDER BY p, v;
+ QUERY PLAN
+------------------------------------------------
+ GroupAggregate
+ Group Key: p, v
+ -> Index Only Scan using btg_p_v_idx on btg
+(3 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, c;
+ QUERY PLAN
+-------------------------------------------------
+ GroupAggregate
+ Group Key: p, c, v
+ -> Incremental Sort
+ Sort Key: p, c, v
+ Presorted Key: p
+ -> Index Scan using btg_p_v_idx on btg
+(6 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, c ORDER BY p, v;
+ QUERY PLAN
+-------------------------------------------------
+ GroupAggregate
+ Group Key: p, v, c
+ -> Incremental Sort
+ Sort Key: p, v, c
+ Presorted Key: p, v
+ -> Index Scan using btg_p_v_idx on btg
+(6 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, c, p, d;
+ QUERY PLAN
+-------------------------------------------------
+ GroupAggregate
+ Group Key: p, c, v, d
+ -> Incremental Sort
+ Sort Key: p, c, v, d
+ Presorted Key: p
+ -> Index Scan using btg_p_v_idx on btg
+(6 rows)
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, c, p, d ORDER BY p, v;
+ QUERY PLAN
+-------------------------------------------------
+ GroupAggregate
+ Group Key: p, v, c, d
+ -> Incremental Sort
+ Sort Key: p, v, c, d
+ Presorted Key: p, v
+ -> Index Scan using btg_p_v_idx on btg
+(6 rows)
+
+DROP TABLE btg;
+RESET enable_hashagg;
+RESET max_parallel_workers;
+RESET max_parallel_workers_per_gather;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
-- Secondly test the case of a parallel aggregate combiner function
-- returning NULL. For that use normal transition function, but a
-- combiner function returning NULL.
diff --git a/src/test/regress/expected/incremental_sort.out b/src/test/regress/expected/incremental_sort.out
index 7fdb685313..b8a0968b06 100644
--- a/src/test/regress/expected/incremental_sort.out
+++ b/src/test/regress/expected/incremental_sort.out
@@ -1426,7 +1426,7 @@ set parallel_setup_cost = 0;
set parallel_tuple_cost = 0;
set max_parallel_workers_per_gather = 2;
create table t (a int, b int, c int);
-insert into t select mod(i,10),mod(i,10),i from generate_series(1,10000) s(i);
+insert into t select mod(i,10),mod(i,10),i from generate_series(1,60000) s(i);
create index on t (a);
analyze t;
set enable_incremental_sort = off;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 9b8638f286..27684bfce2 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -1984,8 +1984,8 @@ USING (name);
------+----+----
bb | 12 | 13
cc | 22 | 23
- dd | | 33
ee | 42 |
+ dd | | 33
(4 rows)
-- Cases with non-nullable expressions in subquery results;
@@ -2019,8 +2019,8 @@ NATURAL FULL JOIN
------+------+------+------+------
bb | 12 | 2 | 13 | 3
cc | 22 | 2 | 23 | 3
- dd | | | 33 | 3
ee | 42 | 2 | |
+ dd | | | 33 | 3
(4 rows)
SELECT * FROM
@@ -5645,18 +5645,20 @@ select d.* from d left join (select * from b group by b.id, b.c_id) s
explain (costs off)
select d.* from d left join (select distinct * from b) s
on d.a = s.id;
- QUERY PLAN
---------------------------------------
- Merge Right Join
- Merge Cond: (b.id = d.a)
- -> Unique
- -> Sort
- Sort Key: b.id, b.c_id
- -> Seq Scan on b
+ QUERY PLAN
+---------------------------------------------
+ Merge Left Join
+ Merge Cond: (d.a = s.id)
-> Sort
Sort Key: d.a
-> Seq Scan on d
-(9 rows)
+ -> Sort
+ Sort Key: s.id
+ -> Subquery Scan on s
+ -> HashAggregate
+ Group Key: b.id, b.c_id
+ -> Seq Scan on b
+(11 rows)
-- join removal is not possible here
explain (costs off)
@@ -7597,44 +7599,39 @@ select * from j1 natural join j2;
explain (verbose, costs off)
select * from j1
inner join (select distinct id from j3) j3 on j1.id = j3.id;
- QUERY PLAN
------------------------------------------
+ QUERY PLAN
+-----------------------------------
Nested Loop
Output: j1.id, j3.id
Inner Unique: true
Join Filter: (j1.id = j3.id)
- -> Unique
+ -> HashAggregate
Output: j3.id
- -> Sort
+ Group Key: j3.id
+ -> Seq Scan on public.j3
Output: j3.id
- Sort Key: j3.id
- -> Seq Scan on public.j3
- Output: j3.id
-> Seq Scan on public.j1
Output: j1.id
-(13 rows)
+(11 rows)
-- ensure group by clause allows the inner to become unique
explain (verbose, costs off)
select * from j1
inner join (select id from j3 group by id) j3 on j1.id = j3.id;
- QUERY PLAN
------------------------------------------
+ QUERY PLAN
+-----------------------------------
Nested Loop
Output: j1.id, j3.id
Inner Unique: true
Join Filter: (j1.id = j3.id)
- -> Group
+ -> HashAggregate
Output: j3.id
Group Key: j3.id
- -> Sort
+ -> Seq Scan on public.j3
Output: j3.id
- Sort Key: j3.id
- -> Seq Scan on public.j3
- Output: j3.id
-> Seq Scan on public.j1
Output: j1.id
-(14 rows)
+(11 rows)
drop table j1;
drop table j2;
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
index 133d42117c..f7c4cc4833 100644
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -1459,18 +1459,15 @@ WHEN MATCHED AND t.a < 10 THEN
explain_merge
--------------------------------------------------------------------
Merge on ex_mtarget t (actual rows=0 loops=1)
- -> Merge Join (actual rows=0 loops=1)
- Merge Cond: (t.a = s.a)
- -> Sort (actual rows=0 loops=1)
- Sort Key: t.a
- Sort Method: quicksort Memory: xxx
+ -> Hash Join (actual rows=0 loops=1)
+ Hash Cond: (s.a = t.a)
+ -> Seq Scan on ex_msource s (actual rows=1 loops=1)
+ -> Hash (actual rows=0 loops=1)
+ Buckets: xxx Batches: xxx Memory Usage: xxx
-> Seq Scan on ex_mtarget t (actual rows=0 loops=1)
Filter: (a < '-1000'::integer)
Rows Removed by Filter: 54
- -> Sort (never executed)
- Sort Key: s.a
- -> Seq Scan on ex_msource s (never executed)
-(12 rows)
+(9 rows)
DROP TABLE ex_msource, ex_mtarget;
DROP FUNCTION explain_merge(text);
diff --git a/src/test/regress/expected/partition_aggregate.out b/src/test/regress/expected/partition_aggregate.out
index 1b900fddf8..800114dfde 100644
--- a/src/test/regress/expected/partition_aggregate.out
+++ b/src/test/regress/expected/partition_aggregate.out
@@ -948,12 +948,12 @@ SET parallel_setup_cost = 0;
-- is not partial agg safe.
EXPLAIN (COSTS OFF)
SELECT a, sum(b), array_agg(distinct c), count(*) FROM pagg_tab_ml GROUP BY a HAVING avg(b) < 3 ORDER BY 1, 2, 3;
- QUERY PLAN
---------------------------------------------------------------------------------------
- Sort
- Sort Key: pagg_tab_ml.a, (sum(pagg_tab_ml.b)), (array_agg(DISTINCT pagg_tab_ml.c))
- -> Gather
- Workers Planned: 2
+ QUERY PLAN
+--------------------------------------------------------------------------------------------
+ Gather Merge
+ Workers Planned: 2
+ -> Sort
+ Sort Key: pagg_tab_ml.a, (sum(pagg_tab_ml.b)), (array_agg(DISTINCT pagg_tab_ml.c))
-> Parallel Append
-> GroupAggregate
Group Key: pagg_tab_ml.a
@@ -1380,28 +1380,26 @@ SELECT x, sum(y), avg(y), count(*) FROM pagg_tab_para GROUP BY x HAVING avg(y) <
-- When GROUP BY clause does not match; partial aggregation is performed for each partition.
EXPLAIN (COSTS OFF)
SELECT y, sum(x), avg(x), count(*) FROM pagg_tab_para GROUP BY y HAVING avg(x) < 12 ORDER BY 1, 2, 3;
- QUERY PLAN
--------------------------------------------------------------------------------------------
+ QUERY PLAN
+-------------------------------------------------------------------------------------
Sort
Sort Key: pagg_tab_para.y, (sum(pagg_tab_para.x)), (avg(pagg_tab_para.x))
- -> Finalize GroupAggregate
+ -> Finalize HashAggregate
Group Key: pagg_tab_para.y
Filter: (avg(pagg_tab_para.x) < '12'::numeric)
- -> Gather Merge
+ -> Gather
Workers Planned: 2
- -> Sort
- Sort Key: pagg_tab_para.y
- -> Parallel Append
- -> Partial HashAggregate
- Group Key: pagg_tab_para.y
- -> Parallel Seq Scan on pagg_tab_para_p1 pagg_tab_para
- -> Partial HashAggregate
- Group Key: pagg_tab_para_1.y
- -> Parallel Seq Scan on pagg_tab_para_p2 pagg_tab_para_1
- -> Partial HashAggregate
- Group Key: pagg_tab_para_2.y
- -> Parallel Seq Scan on pagg_tab_para_p3 pagg_tab_para_2
-(19 rows)
+ -> Parallel Append
+ -> Partial HashAggregate
+ Group Key: pagg_tab_para.y
+ -> Parallel Seq Scan on pagg_tab_para_p1 pagg_tab_para
+ -> Partial HashAggregate
+ Group Key: pagg_tab_para_1.y
+ -> Parallel Seq Scan on pagg_tab_para_p2 pagg_tab_para_1
+ -> Partial HashAggregate
+ Group Key: pagg_tab_para_2.y
+ -> Parallel Seq Scan on pagg_tab_para_p3 pagg_tab_para_2
+(17 rows)
SELECT y, sum(x), avg(x), count(*) FROM pagg_tab_para GROUP BY y HAVING avg(x) < 12 ORDER BY 1, 2, 3;
y | sum | avg | count
diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out
index 6560fe2416..680e82cfc5 100644
--- a/src/test/regress/expected/partition_join.out
+++ b/src/test/regress/expected/partition_join.out
@@ -512,52 +512,41 @@ EXPLAIN (COSTS OFF)
SELECT a, b FROM prt1 FULL JOIN prt2 p2(b,a,c) USING(a,b)
WHERE a BETWEEN 490 AND 510
GROUP BY 1, 2 ORDER BY 1, 2;
- QUERY PLAN
------------------------------------------------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------
Group
Group Key: (COALESCE(prt1.a, p2.a)), (COALESCE(prt1.b, p2.b))
- -> Merge Append
+ -> Sort
Sort Key: (COALESCE(prt1.a, p2.a)), (COALESCE(prt1.b, p2.b))
- -> Group
- Group Key: (COALESCE(prt1.a, p2.a)), (COALESCE(prt1.b, p2.b))
- -> Sort
- Sort Key: (COALESCE(prt1.a, p2.a)), (COALESCE(prt1.b, p2.b))
- -> Merge Full Join
- Merge Cond: ((prt1.a = p2.a) AND (prt1.b = p2.b))
- Filter: ((COALESCE(prt1.a, p2.a) >= 490) AND (COALESCE(prt1.a, p2.a) <= 510))
- -> Sort
- Sort Key: prt1.a, prt1.b
- -> Seq Scan on prt1_p1 prt1
- -> Sort
- Sort Key: p2.a, p2.b
- -> Seq Scan on prt2_p1 p2
- -> Group
- Group Key: (COALESCE(prt1_1.a, p2_1.a)), (COALESCE(prt1_1.b, p2_1.b))
- -> Sort
- Sort Key: (COALESCE(prt1_1.a, p2_1.a)), (COALESCE(prt1_1.b, p2_1.b))
- -> Merge Full Join
- Merge Cond: ((prt1_1.a = p2_1.a) AND (prt1_1.b = p2_1.b))
- Filter: ((COALESCE(prt1_1.a, p2_1.a) >= 490) AND (COALESCE(prt1_1.a, p2_1.a) <= 510))
- -> Sort
- Sort Key: prt1_1.a, prt1_1.b
- -> Seq Scan on prt1_p2 prt1_1
- -> Sort
- Sort Key: p2_1.a, p2_1.b
- -> Seq Scan on prt2_p2 p2_1
- -> Group
- Group Key: (COALESCE(prt1_2.a, p2_2.a)), (COALESCE(prt1_2.b, p2_2.b))
- -> Sort
- Sort Key: (COALESCE(prt1_2.a, p2_2.a)), (COALESCE(prt1_2.b, p2_2.b))
- -> Merge Full Join
- Merge Cond: ((prt1_2.a = p2_2.a) AND (prt1_2.b = p2_2.b))
- Filter: ((COALESCE(prt1_2.a, p2_2.a) >= 490) AND (COALESCE(prt1_2.a, p2_2.a) <= 510))
- -> Sort
- Sort Key: prt1_2.a, prt1_2.b
- -> Seq Scan on prt1_p3 prt1_2
- -> Sort
- Sort Key: p2_2.a, p2_2.b
- -> Seq Scan on prt2_p3 p2_2
-(43 rows)
+ -> Append
+ -> Merge Full Join
+ Merge Cond: ((prt1_1.a = p2_1.a) AND (prt1_1.b = p2_1.b))
+ Filter: ((COALESCE(prt1_1.a, p2_1.a) >= 490) AND (COALESCE(prt1_1.a, p2_1.a) <= 510))
+ -> Sort
+ Sort Key: prt1_1.a, prt1_1.b
+ -> Seq Scan on prt1_p1 prt1_1
+ -> Sort
+ Sort Key: p2_1.a, p2_1.b
+ -> Seq Scan on prt2_p1 p2_1
+ -> Merge Full Join
+ Merge Cond: ((prt1_2.a = p2_2.a) AND (prt1_2.b = p2_2.b))
+ Filter: ((COALESCE(prt1_2.a, p2_2.a) >= 490) AND (COALESCE(prt1_2.a, p2_2.a) <= 510))
+ -> Sort
+ Sort Key: prt1_2.a, prt1_2.b
+ -> Seq Scan on prt1_p2 prt1_2
+ -> Sort
+ Sort Key: p2_2.a, p2_2.b
+ -> Seq Scan on prt2_p2 p2_2
+ -> Merge Full Join
+ Merge Cond: ((prt1_3.a = p2_3.a) AND (prt1_3.b = p2_3.b))
+ Filter: ((COALESCE(prt1_3.a, p2_3.a) >= 490) AND (COALESCE(prt1_3.a, p2_3.a) <= 510))
+ -> Sort
+ Sort Key: prt1_3.a, prt1_3.b
+ -> Seq Scan on prt1_p3 prt1_3
+ -> Sort
+ Sort Key: p2_3.a, p2_3.b
+ -> Seq Scan on prt2_p3 p2_3
+(32 rows)
SELECT a, b FROM prt1 FULL JOIN prt2 p2(b,a,c) USING(a,b)
WHERE a BETWEEN 490 AND 510
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index aae5d51e1c..487fe26906 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -114,6 +114,7 @@ select name, setting from pg_settings where name like 'enable%';
enable_async_append | on
enable_bitmapscan | on
enable_gathermerge | on
+ enable_group_by_reordering | on
enable_hashagg | on
enable_hashjoin | on
enable_incremental_sort | on
@@ -132,7 +133,7 @@ select name, setting from pg_settings where name like 'enable%';
enable_seqscan | on
enable_sort | on
enable_tidscan | on
-(21 rows)
+(22 rows)
-- There are always wait event descriptions for various types.
select type, count(*) > 0 as ok FROM pg_wait_events
diff --git a/src/test/regress/expected/union.out b/src/test/regress/expected/union.out
index e2613d6777..220f0c1629 100644
--- a/src/test/regress/expected/union.out
+++ b/src/test/regress/expected/union.out
@@ -1303,24 +1303,22 @@ select distinct q1 from
union all
select distinct * from int8_tbl i82) ss
where q2 = q2;
- QUERY PLAN
-----------------------------------------------------------
- Unique
- -> Merge Append
- Sort Key: "*SELECT* 1".q1
+ QUERY PLAN
+----------------------------------------------------
+ HashAggregate
+ Group Key: "*SELECT* 1".q1
+ -> Append
-> Subquery Scan on "*SELECT* 1"
- -> Unique
- -> Sort
- Sort Key: i81.q1, i81.q2
- -> Seq Scan on int8_tbl i81
- Filter: (q2 IS NOT NULL)
+ -> HashAggregate
+ Group Key: i81.q1, i81.q2
+ -> Seq Scan on int8_tbl i81
+ Filter: (q2 IS NOT NULL)
-> Subquery Scan on "*SELECT* 2"
- -> Unique
- -> Sort
- Sort Key: i82.q1, i82.q2
- -> Seq Scan on int8_tbl i82
- Filter: (q2 IS NOT NULL)
-(15 rows)
+ -> HashAggregate
+ Group Key: i82.q1, i82.q2
+ -> Seq Scan on int8_tbl i82
+ Filter: (q2 IS NOT NULL)
+(13 rows)
select distinct q1 from
(select distinct * from int8_tbl i81
@@ -1339,24 +1337,22 @@ select distinct q1 from
union all
select distinct * from int8_tbl i82) ss
where -q1 = q2;
- QUERY PLAN
---------------------------------------------------------
- Unique
- -> Merge Append
- Sort Key: "*SELECT* 1".q1
+ QUERY PLAN
+--------------------------------------------------
+ HashAggregate
+ Group Key: "*SELECT* 1".q1
+ -> Append
-> Subquery Scan on "*SELECT* 1"
- -> Unique
- -> Sort
- Sort Key: i81.q1, i81.q2
- -> Seq Scan on int8_tbl i81
- Filter: ((- q1) = q2)
+ -> HashAggregate
+ Group Key: i81.q1, i81.q2
+ -> Seq Scan on int8_tbl i81
+ Filter: ((- q1) = q2)
-> Subquery Scan on "*SELECT* 2"
- -> Unique
- -> Sort
- Sort Key: i82.q1, i82.q2
- -> Seq Scan on int8_tbl i82
- Filter: ((- q1) = q2)
-(15 rows)
+ -> HashAggregate
+ Group Key: i82.q1, i82.q2
+ -> Seq Scan on int8_tbl i82
+ Filter: ((- q1) = q2)
+(13 rows)
select distinct q1 from
(select distinct * from int8_tbl i81
diff --git a/src/test/regress/sql/aggregates.sql b/src/test/regress/sql/aggregates.sql
index 75c78be640..5c4e768825 100644
--- a/src/test/regress/sql/aggregates.sql
+++ b/src/test/regress/sql/aggregates.sql
@@ -1172,6 +1172,105 @@ SELECT balk(hundred) FROM tenk1;
ROLLBACK;
+-- GROUP BY optimization by reorder columns
+
+SELECT
+ i AS id,
+ i/2 AS p,
+ format('%60s', i%2) AS v,
+ i/4 AS c,
+ i/8 AS d,
+ (random() * (10000/8))::int as e --the same as d but no correlation with p
+ INTO btg
+FROM
+ generate_series(1, 10000) i;
+
+VACUUM btg;
+ANALYZE btg;
+
+-- GROUP BY optimization by reorder columns by frequency
+
+SET enable_hashagg=off;
+SET max_parallel_workers= 0;
+SET max_parallel_workers_per_gather = 0;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, v;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, c;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, c ORDER BY v, p, c;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, d, c;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, d, c ORDER BY v, p, d ,c;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, d, c ORDER BY p, v, d ,c;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, d, e;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, e, d;
+
+CREATE STATISTICS btg_dep ON d, e, p FROM btg;
+ANALYZE btg;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, d, e;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, e, d;
+
+
+-- GROUP BY optimization by reorder columns by index scan
+
+CREATE INDEX ON btg(p, v);
+SET enable_seqscan=off;
+SET enable_bitmapscan=off;
+VACUUM btg;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, v;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY p, v ORDER BY p, v;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p ORDER BY p, v;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, c;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, p, c ORDER BY p, v;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, c, p, d;
+
+EXPLAIN (COSTS off)
+SELECT count(*) FROM btg GROUP BY v, c, p, d ORDER BY p, v;
+
+DROP TABLE btg;
+
+RESET enable_hashagg;
+RESET max_parallel_workers;
+RESET max_parallel_workers_per_gather;
+RESET enable_seqscan;
+RESET enable_bitmapscan;
+
+
-- Secondly test the case of a parallel aggregate combiner function
-- returning NULL. For that use normal transition function, but a
-- combiner function returning NULL.
diff --git a/src/test/regress/sql/incremental_sort.sql b/src/test/regress/sql/incremental_sort.sql
index ab471bdfff..70f311a6f2 100644
--- a/src/test/regress/sql/incremental_sort.sql
+++ b/src/test/regress/sql/incremental_sort.sql
@@ -208,7 +208,7 @@ set parallel_tuple_cost = 0;
set max_parallel_workers_per_gather = 2;
create table t (a int, b int, c int);
-insert into t select mod(i,10),mod(i,10),i from generate_series(1,10000) s(i);
+insert into t select mod(i,10),mod(i,10),i from generate_series(1,60000) s(i);
create index on t (a);
analyze t;
--
2.42.0
[text/plain] postgres_fdw.diff (1.9K, ../../[email protected]/3-postgres_fdw.diff)
download | inline diff:
-- join with pseudoconstant quals
EXPLAIN (VERBOSE, COSTS OFF)
SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1 AND CURRENT_USER = SESSION_USER) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10;
- QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ QUERY PLAN
+-----------------------------------------------------------------------------------------------------------------------------------------------
Limit
Output: t1.c1, t2.c1, t1.c3
- -> Result
+ -> Sort
Output: t1.c1, t2.c1, t1.c3
- One-Time Filter: (CURRENT_USER = SESSION_USER)
- -> Foreign Scan
- Output: t1.c1, t1.c3, t2.c1
- Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
- Remote SQL: SELECT r1."C 1", r2."C 1", r1.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r1."C 1")))) ORDER BY r1.c3 ASC NULLS LAST, r1."C 1" ASC NULLS LAST
-(9 rows)
+ Sort Key: t1.c3, t1.c1
+ -> Result
+ Output: t1.c1, t2.c1, t1.c3
+ One-Time Filter: (CURRENT_USER = SESSION_USER)
+ -> Foreign Scan
+ Output: t1.c1, t1.c3, t2.c1
+ Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2)
+ Remote SQL: SELECT r1."C 1", r2."C 1", r1.c3 FROM ("S 1"."T 1" r1 INNER JOIN "S 1"."T 1" r2 ON (((r2."C 1" = r1."C 1"))))
+(12 rows)
-- non-Var items in targetlist of the nullable rel of a join preventing
-- push-down in some cases
^ permalink raw reply [nested|flat] 58+ messages in thread
end of thread, other threads:[~2023-09-13 05:21 UTC | newest]
Thread overview: 58+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2018-10-01 06:03 Re: partition tree inspection functions Michael Paquier <[email protected]>
2018-10-01 06:16 ` Amit Langote <[email protected]>
2018-10-01 06:27 ` Michael Paquier <[email protected]>
2018-10-01 07:27 ` Amit Langote <[email protected]>
2018-10-03 03:37 ` Michael Paquier <[email protected]>
2018-10-03 12:12 ` Jesper Pedersen <[email protected]>
2018-10-04 00:27 ` Michael Paquier <[email protected]>
2018-10-04 07:53 ` Amit Langote <[email protected]>
2018-10-04 08:18 ` Michael Paquier <[email protected]>
2018-10-05 05:56 ` Michael Paquier <[email protected]>
2018-10-05 06:05 ` Pavel Stehule <[email protected]>
2018-10-05 06:31 ` Amit Langote <[email protected]>
2018-10-05 06:52 ` Michael Paquier <[email protected]>
2018-10-05 12:22 ` Jesper Pedersen <[email protected]>
2018-10-06 06:26 ` Michael Paquier <[email protected]>
2018-10-09 08:11 ` Amit Langote <[email protected]>
2018-10-09 09:10 ` Michael Paquier <[email protected]>
2018-10-09 09:41 ` Amit Langote <[email protected]>
2018-10-09 10:05 ` Michael Paquier <[email protected]>
2018-10-09 10:20 ` Amit Langote <[email protected]>
2018-10-09 11:17 ` Michael Paquier <[email protected]>
2018-10-09 11:25 ` Michael Paquier <[email protected]>
2018-10-10 02:54 ` Amit Langote <[email protected]>
2018-10-10 04:01 ` Michael Paquier <[email protected]>
2018-10-19 04:05 ` Amit Langote <[email protected]>
2018-10-19 07:47 ` Michael Paquier <[email protected]>
2018-10-19 09:55 ` Amit Langote <[email protected]>
2018-10-29 03:59 ` Michael Paquier <[email protected]>
2018-10-29 07:08 ` Amit Langote <[email protected]>
2018-10-30 01:33 ` Michael Paquier <[email protected]>
2018-10-30 01:53 ` Amit Langote <[email protected]>
2018-10-04 07:49 ` Amit Langote <[email protected]>
2018-10-12 16:39 ` Robert Haas <[email protected]>
2018-10-12 16:42 ` Alvaro Herrera <[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 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 v18 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 v14 7/7] 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 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 v18 5/5] 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 v18 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 v16 5/5] introduce restore_library Nathan Bossart <[email protected]>
2023-07-24 13:11 Re: POC: GROUP BY optimization Tomas Vondra <[email protected]>
2023-09-13 05:21 ` Re: POC: GROUP BY optimization Andrey Lepikhov <[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