Skip to content

Commit fd28cc2

Browse files
Zhijie HouCommitfest Bot
authored andcommitted
Add a retain_conflict_info option to subscriptions
This patch adds a subscription option allowing users to specify whether information on the subscriber, which is useful for detecting update_deleted conflicts, should be retained. The default setting is false. If set to true, the detection of update_deleted will be enabled, and an additional replication slot named pg_conflict_detection will be created on the subscriber to prevent conflict information from being removed. Note that if multiple subscriptions on one node enable this option, only one replication slot will be created. The logical launcher will create and maintain a replication slot named pg_conflict_detection only if any local subscription has the retain_conflict_info option enabled. Enabling retain_conflict_info is prohibited if the publisher is currently in recovery mode (operating as a standby server). Bump catalog version
1 parent 9b44a50 commit fd28cc2

File tree

20 files changed

+512
-129
lines changed

20 files changed

+512
-129
lines changed

doc/src/sgml/catalogs.sgml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8088,6 +8088,18 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
80888088
</para></entry>
80898089
</row>
80908090

8091+
<row>
8092+
<entry role="catalog_table_entry"><para role="column_definition">
8093+
<structfield>subretainconflictinfo</structfield> <type>bool</type>
8094+
</para>
8095+
<para>
8096+
If true, the detection of <xref linkend="conflict-update-deleted"/> is
8097+
enabled and the information (e.g., dead tuples, commit timestamps, and
8098+
origins) on the subscriber that is still useful for conflict detection
8099+
is retained.
8100+
</para></entry>
8101+
</row>
8102+
80918103
<row>
80928104
<entry role="catalog_table_entry"><para role="column_definition">
80938105
<structfield>subconninfo</structfield> <type>text</type>

doc/src/sgml/logical-replication.sgml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2408,7 +2408,8 @@ CONTEXT: processing remote data for replication origin "pg_16395" during "INSER
24082408
<para>
24092409
<link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link>
24102410
must be set to at least the number of subscriptions expected to connect,
2411-
plus some reserve for table synchronization.
2411+
plus some reserve for table synchronization and one if
2412+
<link linkend="sql-createsubscription-params-with-retain-conflict-info"><literal>retain_conflict_info</literal></link> is enabled.
24122413
</para>
24132414

24142415
<para>

doc/src/sgml/ref/alter_subscription.sgml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,9 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
235235
<link linkend="sql-createsubscription-params-with-password-required"><literal>password_required</literal></link>,
236236
<link linkend="sql-createsubscription-params-with-run-as-owner"><literal>run_as_owner</literal></link>,
237237
<link linkend="sql-createsubscription-params-with-origin"><literal>origin</literal></link>,
238-
<link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>, and
239-
<link linkend="sql-createsubscription-params-with-two-phase"><literal>two_phase</literal></link>.
238+
<link linkend="sql-createsubscription-params-with-failover"><literal>failover</literal></link>,
239+
<link linkend="sql-createsubscription-params-with-two-phase"><literal>two_phase</literal></link>, and
240+
<link linkend="sql-createsubscription-params-with-retain-conflict-info"><literal>retain_conflict_info</literal></link>.
240241
Only a superuser can set <literal>password_required = false</literal>.
241242
</para>
242243

@@ -285,6 +286,13 @@ ALTER SUBSCRIPTION <replaceable class="parameter">name</replaceable> RENAME TO <
285286
option is changed from <literal>true</literal> to <literal>false</literal>,
286287
the publisher will replicate the transactions again when they are committed.
287288
</para>
289+
290+
<para>
291+
If the <link linkend="sql-createsubscription-params-with-retain-conflict-info"><literal>retain_conflict_info</literal></link>
292+
option is altered to <literal>false</literal> and no other subscription
293+
has this option enabled, the additional replication slot that was created
294+
to retain conflict information will be dropped.
295+
</para>
288296
</listitem>
289297
</varlistentry>
290298

doc/src/sgml/ref/create_subscription.sgml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,35 @@ CREATE SUBSCRIPTION <replaceable class="parameter">subscription_name</replaceabl
437437
</para>
438438
</listitem>
439439
</varlistentry>
440+
441+
<varlistentry id="sql-createsubscription-params-with-retain-conflict-info">
442+
<term><literal>retain_conflict_info</literal> (<type>boolean</type>)</term>
443+
<listitem>
444+
<para>
445+
Specifies whether the information (e.g., dead tuples, commit
446+
timestamps, and origins) on the subscriber that is still useful for
447+
conflict detection is retained. The default is
448+
<literal>false</literal>. If set to true, the detection of
449+
<xref linkend="conflict-update-deleted"/> is enabled, and an
450+
additional replication slot named
451+
<quote><literal>pg_conflict_detection</literal></quote> will be
452+
created on the subscriber to prevent the conflict information from
453+
being removed.
454+
</para>
455+
456+
<para>
457+
Note that the information useful for conflict detection is retained
458+
only after the creation of the additional slot. You can verify the
459+
existence of this slot by querying <link linkend="view-pg-replication-slots">pg_replication_slots</link>.<structfield>conflicting</structfield>
460+
And even if multiple subscriptions on one node enable this option,
461+
only one replication slot will be created.
462+
</para>
463+
464+
<para>
465+
This option cannot be enabled if the publisher is also a physical standby.
466+
</para>
467+
</listitem>
468+
</varlistentry>
440469
</variablelist></para>
441470

442471
</listitem>

src/backend/catalog/pg_subscription.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ GetSubscription(Oid subid, bool missing_ok)
103103
sub->passwordrequired = subform->subpasswordrequired;
104104
sub->runasowner = subform->subrunasowner;
105105
sub->failover = subform->subfailover;
106+
sub->retainconflictinfo = subform->subretainconflictinfo;
106107

107108
/* Get conninfo */
108109
datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID,

src/backend/catalog/system_views.sql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1383,7 +1383,8 @@ REVOKE ALL ON pg_subscription FROM public;
13831383
GRANT SELECT (oid, subdbid, subskiplsn, subname, subowner, subenabled,
13841384
subbinary, substream, subtwophasestate, subdisableonerr,
13851385
subpasswordrequired, subrunasowner, subfailover,
1386-
subslotname, subsynccommit, subpublications, suborigin)
1386+
subretainconflictinfo, subslotname, subsynccommit,
1387+
subpublications, suborigin)
13871388
ON pg_subscription TO public;
13881389

13891390
CREATE VIEW pg_stat_subscription_stats AS

src/backend/commands/subscriptioncmds.c

Lines changed: 138 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
#include "postgres.h"
1616

17+
#include "access/commit_ts.h"
1718
#include "access/htup_details.h"
1819
#include "access/table.h"
1920
#include "access/twophase.h"
@@ -71,8 +72,9 @@
7172
#define SUBOPT_PASSWORD_REQUIRED 0x00000800
7273
#define SUBOPT_RUN_AS_OWNER 0x00001000
7374
#define SUBOPT_FAILOVER 0x00002000
74-
#define SUBOPT_LSN 0x00004000
75-
#define SUBOPT_ORIGIN 0x00008000
75+
#define SUBOPT_RETAIN_CONFLICT_INFO 0x00004000
76+
#define SUBOPT_LSN 0x00008000
77+
#define SUBOPT_ORIGIN 0x00010000
7678

7779
/* check if the 'val' has 'bits' set */
7880
#define IsSet(val, bits) (((val) & (bits)) == (bits))
@@ -98,6 +100,7 @@ typedef struct SubOpts
98100
bool passwordrequired;
99101
bool runasowner;
100102
bool failover;
103+
bool retainconflictinfo;
101104
char *origin;
102105
XLogRecPtr lsn;
103106
} SubOpts;
@@ -107,6 +110,8 @@ static void check_publications_origin(WalReceiverConn *wrconn,
107110
List *publications, bool copydata,
108111
char *origin, Oid *subrel_local_oids,
109112
int subrel_count, char *subname);
113+
static void check_conflict_info_retaintion(WalReceiverConn *wrconn,
114+
bool retain_conflict_info);
110115
static void check_duplicates_in_publist(List *publist, Datum *datums);
111116
static List *merge_publications(List *oldpublist, List *newpublist, bool addpub, const char *subname);
112117
static void ReportSlotConnectionError(List *rstates, Oid subid, char *slotname, char *err);
@@ -162,6 +167,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
162167
opts->runasowner = false;
163168
if (IsSet(supported_opts, SUBOPT_FAILOVER))
164169
opts->failover = false;
170+
if (IsSet(supported_opts, SUBOPT_RETAIN_CONFLICT_INFO))
171+
opts->retainconflictinfo = false;
165172
if (IsSet(supported_opts, SUBOPT_ORIGIN))
166173
opts->origin = pstrdup(LOGICALREP_ORIGIN_ANY);
167174

@@ -307,6 +314,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
307314
opts->specified_opts |= SUBOPT_FAILOVER;
308315
opts->failover = defGetBoolean(defel);
309316
}
317+
else if (IsSet(supported_opts, SUBOPT_RETAIN_CONFLICT_INFO) &&
318+
strcmp(defel->defname, "retain_conflict_info") == 0)
319+
{
320+
if (IsSet(opts->specified_opts, SUBOPT_RETAIN_CONFLICT_INFO))
321+
errorConflictingDefElem(defel, pstate);
322+
323+
opts->specified_opts |= SUBOPT_RETAIN_CONFLICT_INFO;
324+
opts->retainconflictinfo = defGetBoolean(defel);
325+
}
310326
else if (IsSet(supported_opts, SUBOPT_ORIGIN) &&
311327
strcmp(defel->defname, "origin") == 0)
312328
{
@@ -563,7 +579,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
563579
SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
564580
SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
565581
SUBOPT_DISABLE_ON_ERR | SUBOPT_PASSWORD_REQUIRED |
566-
SUBOPT_RUN_AS_OWNER | SUBOPT_FAILOVER | SUBOPT_ORIGIN);
582+
SUBOPT_RUN_AS_OWNER | SUBOPT_FAILOVER |
583+
SUBOPT_RETAIN_CONFLICT_INFO | SUBOPT_ORIGIN);
567584
parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
568585

569586
/*
@@ -608,6 +625,12 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
608625
errmsg("password_required=false is superuser-only"),
609626
errhint("Subscriptions with the password_required option set to false may only be created or modified by the superuser.")));
610627

628+
if (opts.retainconflictinfo && !track_commit_timestamp)
629+
ereport(WARNING,
630+
errcode(ERRCODE_INVALID_PARAMETER_VALUE),
631+
errmsg("information for detecting conflicts cannot be fully retained when \"%s\" is disabled",
632+
"track_commit_timestamp"));
633+
611634
/*
612635
* If built with appropriate switch, whine when regression-testing
613636
* conventions for subscription names are violated.
@@ -670,6 +693,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
670693
values[Anum_pg_subscription_subpasswordrequired - 1] = BoolGetDatum(opts.passwordrequired);
671694
values[Anum_pg_subscription_subrunasowner - 1] = BoolGetDatum(opts.runasowner);
672695
values[Anum_pg_subscription_subfailover - 1] = BoolGetDatum(opts.failover);
696+
values[Anum_pg_subscription_subretainconflictinfo - 1] =
697+
BoolGetDatum(opts.retainconflictinfo);
673698
values[Anum_pg_subscription_subconninfo - 1] =
674699
CStringGetTextDatum(conninfo);
675700
if (opts.slot_name)
@@ -724,6 +749,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
724749
check_publications_origin(wrconn, publications, opts.copy_data,
725750
opts.origin, NULL, 0, stmt->subname);
726751

752+
check_conflict_info_retaintion(wrconn, opts.retainconflictinfo);
753+
727754
/*
728755
* Set sync state based on if we were asked to do data copy or
729756
* not.
@@ -1110,6 +1137,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
11101137
bool update_tuple = false;
11111138
bool update_failover = false;
11121139
bool update_two_phase = false;
1140+
bool retain_conflict_info = false;
11131141
Subscription *sub;
11141142
Form_pg_subscription form;
11151143
bits32 supported_opts;
@@ -1165,7 +1193,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
11651193
SUBOPT_DISABLE_ON_ERR |
11661194
SUBOPT_PASSWORD_REQUIRED |
11671195
SUBOPT_RUN_AS_OWNER | SUBOPT_FAILOVER |
1168-
SUBOPT_ORIGIN);
1196+
SUBOPT_RETAIN_CONFLICT_INFO | SUBOPT_ORIGIN);
11691197

11701198
parse_subscription_options(pstate, stmt->options,
11711199
supported_opts, &opts);
@@ -1325,6 +1353,29 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
13251353
replaces[Anum_pg_subscription_subfailover - 1] = true;
13261354
}
13271355

1356+
if (IsSet(opts.specified_opts, SUBOPT_RETAIN_CONFLICT_INFO))
1357+
{
1358+
values[Anum_pg_subscription_subretainconflictinfo - 1] =
1359+
BoolGetDatum(opts.retainconflictinfo);
1360+
replaces[Anum_pg_subscription_subretainconflictinfo - 1] = true;
1361+
1362+
if (opts.retainconflictinfo && !track_commit_timestamp)
1363+
ereport(WARNING,
1364+
errcode(ERRCODE_INVALID_PARAMETER_VALUE),
1365+
errmsg("information for detecting conflicts cannot be fully retained when \"%s\" is disabled",
1366+
"track_commit_timestamp"));
1367+
1368+
/*
1369+
* Notify the launcher to manage the replication slot for
1370+
* conflict detection. This ensures that replication slot
1371+
* is efficiently handled (created, updated, or dropped)
1372+
* in response to any configuration changes.
1373+
*/
1374+
ApplyLauncherWakeupAtCommit();
1375+
1376+
retain_conflict_info = opts.retainconflictinfo;
1377+
}
1378+
13281379
if (IsSet(opts.specified_opts, SUBOPT_ORIGIN))
13291380
{
13301381
values[Anum_pg_subscription_suborigin - 1] =
@@ -1355,6 +1406,14 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
13551406
ApplyLauncherWakeupAtCommit();
13561407

13571408
update_tuple = true;
1409+
1410+
/*
1411+
* The subscription might be initially created with
1412+
* connect=false and retain_conflict_info=true, meaning the
1413+
* remote server's status may not be checked. Ensure this
1414+
* check is conducted now.
1415+
*/
1416+
retain_conflict_info = sub->retainconflictinfo;
13581417
break;
13591418
}
13601419

@@ -1369,6 +1428,13 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
13691428
CStringGetTextDatum(stmt->conninfo);
13701429
replaces[Anum_pg_subscription_subconninfo - 1] = true;
13711430
update_tuple = true;
1431+
1432+
/*
1433+
* Since the remote server configuration might have changed,
1434+
* perform a check to ensure it permits enabling
1435+
* retain_conflict_info.
1436+
*/
1437+
retain_conflict_info = sub->retainconflictinfo;
13721438
break;
13731439

13741440
case ALTER_SUBSCRIPTION_SET_PUBLICATION:
@@ -1568,14 +1634,15 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
15681634
}
15691635

15701636
/*
1571-
* Try to acquire the connection necessary for altering the slot, if
1572-
* needed.
1637+
* Try to acquire the connection necessary either for modifying the slot
1638+
* or for checking if the remote server permits enabling
1639+
* retain_conflict_info.
15731640
*
15741641
* This has to be at the end because otherwise if there is an error while
15751642
* doing the database operations we won't be able to rollback altered
15761643
* slot.
15771644
*/
1578-
if (update_failover || update_two_phase)
1645+
if (update_failover || update_two_phase || retain_conflict_info)
15791646
{
15801647
bool must_use_password;
15811648
char *err;
@@ -1584,10 +1651,14 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
15841651
/* Load the library providing us libpq calls. */
15851652
load_file("libpqwalreceiver", false);
15861653

1587-
/* Try to connect to the publisher. */
1654+
/*
1655+
* Try to connect to the publisher, using the new connection string if
1656+
* available.
1657+
*/
15881658
must_use_password = sub->passwordrequired && !sub->ownersuperuser;
1589-
wrconn = walrcv_connect(sub->conninfo, true, true, must_use_password,
1590-
sub->name, &err);
1659+
wrconn = walrcv_connect(stmt->conninfo ? stmt->conninfo : sub->conninfo,
1660+
true, true, must_use_password, sub->name,
1661+
&err);
15911662
if (!wrconn)
15921663
ereport(ERROR,
15931664
(errcode(ERRCODE_CONNECTION_FAILURE),
@@ -1596,9 +1667,12 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
15961667

15971668
PG_TRY();
15981669
{
1599-
walrcv_alter_slot(wrconn, sub->slotname,
1600-
update_failover ? &opts.failover : NULL,
1601-
update_two_phase ? &opts.twophase : NULL);
1670+
check_conflict_info_retaintion(wrconn, retain_conflict_info);
1671+
1672+
if (update_failover || update_two_phase)
1673+
walrcv_alter_slot(wrconn, sub->slotname,
1674+
update_failover ? &opts.failover : NULL,
1675+
update_two_phase ? &opts.twophase : NULL);
16021676
}
16031677
PG_FINALLY();
16041678
{
@@ -2196,6 +2270,57 @@ check_publications_origin(WalReceiverConn *wrconn, List *publications,
21962270
walrcv_clear_result(res);
21972271
}
21982272

2273+
/*
2274+
* Check if the publisher's status permits enabling retain_conflict_info.
2275+
*
2276+
* Enabling retain_conflict_info is not allowed if the publisher's version is
2277+
* prior to PG18 or if the publisher is in recovery (operating as a standby
2278+
* server).
2279+
*
2280+
* Refer to the comments atop maybe_advance_nonremovable_xid() for detailed
2281+
* reasons.
2282+
*/
2283+
static void
2284+
check_conflict_info_retaintion(WalReceiverConn *wrconn, bool retain_conflict_info)
2285+
{
2286+
WalRcvExecResult *res;
2287+
Oid RecoveryRow[1] = {BOOLOID};
2288+
TupleTableSlot *slot;
2289+
bool isnull;
2290+
bool remote_in_recovery;
2291+
2292+
if (!retain_conflict_info)
2293+
return;
2294+
2295+
if (walrcv_server_version(wrconn) < 18000)
2296+
ereport(ERROR,
2297+
errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
2298+
errmsg("cannot enable retain_conflict_info if the publisher is running a version earlier than PostgreSQL 18."));
2299+
2300+
res = walrcv_exec(wrconn, "SELECT pg_is_in_recovery()", 1, RecoveryRow);
2301+
2302+
if (res->status != WALRCV_OK_TUPLES)
2303+
ereport(ERROR,
2304+
(errcode(ERRCODE_CONNECTION_FAILURE),
2305+
errmsg("could not obtain recovery progress from the publisher: %s",
2306+
res->err)));
2307+
2308+
slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
2309+
if (!tuplestore_gettupleslot(res->tuplestore, true, false, slot))
2310+
elog(ERROR, "failed to fetch tuple for the recovery progress");
2311+
2312+
remote_in_recovery = DatumGetBool(slot_getattr(slot, 1, &isnull));
2313+
2314+
if (remote_in_recovery)
2315+
ereport(ERROR,
2316+
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
2317+
errmsg("cannot enable retain_conflict_info if the publisher is in recovery."));
2318+
2319+
ExecDropSingleTupleTableSlot(slot);
2320+
2321+
walrcv_clear_result(res);
2322+
}
2323+
21992324
/*
22002325
* Get the list of tables which belong to specified publications on the
22012326
* publisher connection.

0 commit comments

Comments
 (0)