1
0
mirror of https://github.com/postgres/postgres.git synced 2025-10-31 10:30:33 +03:00

Preserve conflict-relevant data during logical replication.

Logical replication requires reliable conflict detection to maintain data
consistency across nodes. To achieve this, we must prevent premature
removal of tuples deleted by other origins and their associated commit_ts
data by VACUUM, which could otherwise lead to incorrect conflict reporting
and resolution.

This patch introduces a mechanism to retain deleted tuples on the
subscriber during the application of concurrent transactions from remote
nodes. Retaining these tuples allows us to correctly ignore concurrent
updates to the same tuple. Without this, an UPDATE might be misinterpreted
as an INSERT during resolutions due to the absence of the original tuple.

Additionally, we ensure that origin metadata is not prematurely removed by
vacuum freeze, which is essential for detecting update_origin_differs and
delete_origin_differs conflicts.

To support this, a new replication slot named pg_conflict_detection is
created and maintained by the launcher on the subscriber. Each apply
worker tracks its own non-removable transaction ID, which the launcher
aggregates to determine the appropriate xmin for the slot, thereby
retaining necessary tuples.

Conflict information retention (deleted tuples and commit_ts) can be
enabled per subscription via the retain_conflict_info option. This is
disabled by default to avoid unnecessary overhead for configurations that
do not require conflict resolution or logging.

During upgrades, if any subscription on the old cluster has
retain_conflict_info enabled, a conflict detection slot will be created to
protect relevant tuples from deletion when the new cluster starts.

This is a foundational work to correctly detect update_deleted conflict
which will be done in a follow-up patch.

Author: Zhijie Hou <houzj.fnst@fujitsu.com>
Reviewed-by: shveta malik <shveta.malik@gmail.com>
Reviewed-by: Masahiko Sawada <sawada.mshk@gmail.com>
Reviewed-by: Dilip Kumar <dilipbalaut@gmail.com>
Reviewed-by: Nisha Moond <nisha.moond412@gmail.com>
Reviewed-by: Amit Kapila <amit.kapila16@gmail.com>
Discussion: https://postgr.es/m/OS0PR01MB5716BE80DAEB0EE2A6A5D1F5949D2@OS0PR01MB5716.jpnprd01.prod.outlook.com
This commit is contained in:
Amit Kapila
2025-07-23 02:56:00 +00:00
parent 039f7ee0fe
commit 228c370868
45 changed files with 2234 additions and 221 deletions

View File

@@ -14,6 +14,7 @@
#include "postgres.h"
#include "access/commit_ts.h"
#include "access/htup_details.h"
#include "access/table.h"
#include "access/twophase.h"
@@ -71,8 +72,9 @@
#define SUBOPT_PASSWORD_REQUIRED 0x00000800
#define SUBOPT_RUN_AS_OWNER 0x00001000
#define SUBOPT_FAILOVER 0x00002000
#define SUBOPT_LSN 0x00004000
#define SUBOPT_ORIGIN 0x00008000
#define SUBOPT_RETAIN_DEAD_TUPLES 0x00004000
#define SUBOPT_LSN 0x00008000
#define SUBOPT_ORIGIN 0x00010000
/* check if the 'val' has 'bits' set */
#define IsSet(val, bits) (((val) & (bits)) == (bits))
@@ -98,6 +100,7 @@ typedef struct SubOpts
bool passwordrequired;
bool runasowner;
bool failover;
bool retaindeadtuples;
char *origin;
XLogRecPtr lsn;
} SubOpts;
@@ -105,8 +108,10 @@ typedef struct SubOpts
static List *fetch_table_list(WalReceiverConn *wrconn, List *publications);
static void check_publications_origin(WalReceiverConn *wrconn,
List *publications, bool copydata,
char *origin, Oid *subrel_local_oids,
int subrel_count, char *subname);
bool retain_dead_tuples, char *origin,
Oid *subrel_local_oids, int subrel_count,
char *subname);
static void check_pub_dead_tuple_retention(WalReceiverConn *wrconn);
static void check_duplicates_in_publist(List *publist, Datum *datums);
static List *merge_publications(List *oldpublist, List *newpublist, bool addpub, const char *subname);
static void ReportSlotConnectionError(List *rstates, Oid subid, char *slotname, char *err);
@@ -162,6 +167,8 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
opts->runasowner = false;
if (IsSet(supported_opts, SUBOPT_FAILOVER))
opts->failover = false;
if (IsSet(supported_opts, SUBOPT_RETAIN_DEAD_TUPLES))
opts->retaindeadtuples = false;
if (IsSet(supported_opts, SUBOPT_ORIGIN))
opts->origin = pstrdup(LOGICALREP_ORIGIN_ANY);
@@ -210,7 +217,7 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
if (strcmp(opts->slot_name, "none") == 0)
opts->slot_name = NULL;
else
ReplicationSlotValidateName(opts->slot_name, ERROR);
ReplicationSlotValidateName(opts->slot_name, false, ERROR);
}
else if (IsSet(supported_opts, SUBOPT_COPY_DATA) &&
strcmp(defel->defname, "copy_data") == 0)
@@ -307,6 +314,15 @@ parse_subscription_options(ParseState *pstate, List *stmt_options,
opts->specified_opts |= SUBOPT_FAILOVER;
opts->failover = defGetBoolean(defel);
}
else if (IsSet(supported_opts, SUBOPT_RETAIN_DEAD_TUPLES) &&
strcmp(defel->defname, "retain_dead_tuples") == 0)
{
if (IsSet(opts->specified_opts, SUBOPT_RETAIN_DEAD_TUPLES))
errorConflictingDefElem(defel, pstate);
opts->specified_opts |= SUBOPT_RETAIN_DEAD_TUPLES;
opts->retaindeadtuples = defGetBoolean(defel);
}
else if (IsSet(supported_opts, SUBOPT_ORIGIN) &&
strcmp(defel->defname, "origin") == 0)
{
@@ -563,7 +579,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
SUBOPT_SYNCHRONOUS_COMMIT | SUBOPT_BINARY |
SUBOPT_STREAMING | SUBOPT_TWOPHASE_COMMIT |
SUBOPT_DISABLE_ON_ERR | SUBOPT_PASSWORD_REQUIRED |
SUBOPT_RUN_AS_OWNER | SUBOPT_FAILOVER | SUBOPT_ORIGIN);
SUBOPT_RUN_AS_OWNER | SUBOPT_FAILOVER |
SUBOPT_RETAIN_DEAD_TUPLES | SUBOPT_ORIGIN);
parse_subscription_options(pstate, stmt->options, supported_opts, &opts);
/*
@@ -630,6 +647,10 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
stmt->subname)));
}
/* Ensure that we can enable retain_dead_tuples */
if (opts.retaindeadtuples)
CheckSubDeadTupleRetention(true, !opts.enabled, WARNING);
if (!IsSet(opts.specified_opts, SUBOPT_SLOT_NAME) &&
opts.slot_name == NULL)
opts.slot_name = stmt->subname;
@@ -670,6 +691,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
values[Anum_pg_subscription_subpasswordrequired - 1] = BoolGetDatum(opts.passwordrequired);
values[Anum_pg_subscription_subrunasowner - 1] = BoolGetDatum(opts.runasowner);
values[Anum_pg_subscription_subfailover - 1] = BoolGetDatum(opts.failover);
values[Anum_pg_subscription_subretaindeadtuples - 1] =
BoolGetDatum(opts.retaindeadtuples);
values[Anum_pg_subscription_subconninfo - 1] =
CStringGetTextDatum(conninfo);
if (opts.slot_name)
@@ -722,7 +745,11 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt,
{
check_publications(wrconn, publications);
check_publications_origin(wrconn, publications, opts.copy_data,
opts.origin, NULL, 0, stmt->subname);
opts.retaindeadtuples, opts.origin,
NULL, 0, stmt->subname);
if (opts.retaindeadtuples)
check_pub_dead_tuple_retention(wrconn);
/*
* Set sync state based on if we were asked to do data copy or
@@ -881,8 +908,8 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
sizeof(Oid), oid_cmp);
check_publications_origin(wrconn, sub->publications, copy_data,
sub->origin, subrel_local_oids,
subrel_count, sub->name);
sub->retaindeadtuples, sub->origin,
subrel_local_oids, subrel_count, sub->name);
/*
* Rels that we want to remove from subscription and drop any slots
@@ -1040,18 +1067,22 @@ AlterSubscription_refresh(Subscription *sub, bool copy_data,
}
/*
* Common checks for altering failover and two_phase options.
* Common checks for altering failover, two_phase, and retain_dead_tuples
* options.
*/
static void
CheckAlterSubOption(Subscription *sub, const char *option,
bool slot_needs_update, bool isTopLevel)
{
/*
* The checks in this function are required only for failover and
* two_phase options.
*/
Assert(strcmp(option, "failover") == 0 ||
strcmp(option, "two_phase") == 0);
strcmp(option, "two_phase") == 0 ||
strcmp(option, "retain_dead_tuples") == 0);
/*
* Altering the retain_dead_tuples option does not update the slot on the
* publisher.
*/
Assert(!slot_needs_update || strcmp(option, "retain_dead_tuples") != 0);
/*
* Do not allow changing the option if the subscription is enabled. This
@@ -1063,6 +1094,39 @@ CheckAlterSubOption(Subscription *sub, const char *option,
* the publisher by the existing walsender, so we could have allowed that
* even when the subscription is enabled. But we kept this restriction for
* the sake of consistency and simplicity.
*
* Additionally, do not allow changing the retain_dead_tuples option when
* the subscription is enabled to prevent race conditions arising from the
* new option value being acknowledged asynchronously by the launcher and
* apply workers.
*
* Without the restriction, a race condition may arise when a user
* disables and immediately re-enables the retain_dead_tuples option. In
* this case, the launcher might drop the slot upon noticing the disabled
* action, while the apply worker may keep maintaining
* oldest_nonremovable_xid without noticing the option change. During this
* period, a transaction ID wraparound could falsely make this ID appear
* as if it originates from the future w.r.t the transaction ID stored in
* the slot maintained by launcher.
*
* Similarly, if the user enables retain_dead_tuples concurrently with the
* launcher starting the worker, the apply worker may start calculating
* oldest_nonremovable_xid before the launcher notices the enable action.
* Consequently, the launcher may update slot.xmin to a newer value than
* that maintained by the worker. In subsequent cycles, upon integrating
* the worker's oldest_nonremovable_xid, the launcher might detect a
* retreat in the calculated xmin, necessitating additional handling.
*
* XXX To address the above race conditions, we can define
* oldest_nonremovable_xid as FullTransactionID and adds the check to
* disallow retreating the conflict slot's xmin. For now, we kept the
* implementation simple by disallowing change to the retain_dead_tuples,
* but in the future we can change this after some more analysis.
*
* Note that we could restrict only the enabling of retain_dead_tuples to
* avoid the race conditions described above, but we maintain the
* restriction for both enable and disable operations for the sake of
* consistency.
*/
if (sub->enabled)
ereport(ERROR,
@@ -1110,6 +1174,9 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
bool update_tuple = false;
bool update_failover = false;
bool update_two_phase = false;
bool check_pub_rdt = false;
bool retain_dead_tuples;
char *origin;
Subscription *sub;
Form_pg_subscription form;
bits32 supported_opts;
@@ -1137,6 +1204,9 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
sub = GetSubscription(subid, false);
retain_dead_tuples = sub->retaindeadtuples;
origin = sub->origin;
/*
* Don't allow non-superuser modification of a subscription with
* password_required=false.
@@ -1165,7 +1235,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
SUBOPT_DISABLE_ON_ERR |
SUBOPT_PASSWORD_REQUIRED |
SUBOPT_RUN_AS_OWNER | SUBOPT_FAILOVER |
SUBOPT_ORIGIN);
SUBOPT_RETAIN_DEAD_TUPLES | SUBOPT_ORIGIN);
parse_subscription_options(pstate, stmt->options,
supported_opts, &opts);
@@ -1325,11 +1395,62 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
replaces[Anum_pg_subscription_subfailover - 1] = true;
}
if (IsSet(opts.specified_opts, SUBOPT_RETAIN_DEAD_TUPLES))
{
values[Anum_pg_subscription_subretaindeadtuples - 1] =
BoolGetDatum(opts.retaindeadtuples);
replaces[Anum_pg_subscription_subretaindeadtuples - 1] = true;
CheckAlterSubOption(sub, "retain_dead_tuples", false, isTopLevel);
/*
* Workers may continue running even after the
* subscription has been disabled.
*
* To prevent race conditions (as described in
* CheckAlterSubOption()), ensure that all worker
* processes have already exited before proceeding.
*/
if (logicalrep_workers_find(subid, true, true))
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot alter retain_dead_tuples when logical replication worker is still running"),
errhint("Try again after some time.")));
/*
* Remind the user that enabling subscription will prevent
* the accumulation of dead tuples.
*/
if (opts.retaindeadtuples)
CheckSubDeadTupleRetention(true, !sub->enabled, NOTICE);
/*
* Notify the launcher to manage the replication slot for
* conflict detection. This ensures that replication slot
* is efficiently handled (created, updated, or dropped)
* in response to any configuration changes.
*/
ApplyLauncherWakeupAtCommit();
check_pub_rdt = opts.retaindeadtuples;
retain_dead_tuples = opts.retaindeadtuples;
}
if (IsSet(opts.specified_opts, SUBOPT_ORIGIN))
{
values[Anum_pg_subscription_suborigin - 1] =
CStringGetTextDatum(opts.origin);
replaces[Anum_pg_subscription_suborigin - 1] = true;
/*
* Check if changes from different origins may be received
* from the publisher when the origin is changed to ANY
* and retain_dead_tuples is enabled.
*/
check_pub_rdt = retain_dead_tuples &&
pg_strcasecmp(opts.origin, LOGICALREP_ORIGIN_ANY) == 0;
origin = opts.origin;
}
update_tuple = true;
@@ -1347,6 +1468,15 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot enable subscription that does not have a slot name")));
/*
* Check track_commit_timestamp only when enabling the
* subscription in case it was disabled after creation. See
* comments atop CheckSubDeadTupleRetention() for details.
*/
if (sub->retaindeadtuples)
CheckSubDeadTupleRetention(opts.enabled, !opts.enabled,
WARNING);
values[Anum_pg_subscription_subenabled - 1] =
BoolGetDatum(opts.enabled);
replaces[Anum_pg_subscription_subenabled - 1] = true;
@@ -1355,6 +1485,14 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
ApplyLauncherWakeupAtCommit();
update_tuple = true;
/*
* The subscription might be initially created with
* connect=false and retain_dead_tuples=true, meaning the
* remote server's status may not be checked. Ensure this
* check is conducted now.
*/
check_pub_rdt = sub->retaindeadtuples && opts.enabled;
break;
}
@@ -1369,6 +1507,13 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
CStringGetTextDatum(stmt->conninfo);
replaces[Anum_pg_subscription_subconninfo - 1] = true;
update_tuple = true;
/*
* Since the remote server configuration might have changed,
* perform a check to ensure it permits enabling
* retain_dead_tuples.
*/
check_pub_rdt = sub->retaindeadtuples;
break;
case ALTER_SUBSCRIPTION_SET_PUBLICATION:
@@ -1568,14 +1713,15 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
}
/*
* Try to acquire the connection necessary for altering the slot, if
* needed.
* Try to acquire the connection necessary either for modifying the slot
* or for checking if the remote server permits enabling
* retain_dead_tuples.
*
* This has to be at the end because otherwise if there is an error while
* doing the database operations we won't be able to rollback altered
* slot.
*/
if (update_failover || update_two_phase)
if (update_failover || update_two_phase || check_pub_rdt)
{
bool must_use_password;
char *err;
@@ -1584,10 +1730,14 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
/* Load the library providing us libpq calls. */
load_file("libpqwalreceiver", false);
/* Try to connect to the publisher. */
/*
* Try to connect to the publisher, using the new connection string if
* available.
*/
must_use_password = sub->passwordrequired && !sub->ownersuperuser;
wrconn = walrcv_connect(sub->conninfo, true, true, must_use_password,
sub->name, &err);
wrconn = walrcv_connect(stmt->conninfo ? stmt->conninfo : sub->conninfo,
true, true, must_use_password, sub->name,
&err);
if (!wrconn)
ereport(ERROR,
(errcode(ERRCODE_CONNECTION_FAILURE),
@@ -1596,9 +1746,17 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt,
PG_TRY();
{
walrcv_alter_slot(wrconn, sub->slotname,
update_failover ? &opts.failover : NULL,
update_two_phase ? &opts.twophase : NULL);
if (retain_dead_tuples)
check_pub_dead_tuple_retention(wrconn);
check_publications_origin(wrconn, sub->publications, false,
retain_dead_tuples, origin, NULL, 0,
sub->name);
if (update_failover || update_two_phase)
walrcv_alter_slot(wrconn, sub->slotname,
update_failover ? &opts.failover : NULL,
update_two_phase ? &opts.twophase : NULL);
}
PG_FINALLY();
{
@@ -2086,20 +2244,29 @@ AlterSubscriptionOwner_oid(Oid subid, Oid newOwnerId)
* Check and log a warning if the publisher has subscribed to the same table,
* its partition ancestors (if it's a partition), or its partition children (if
* it's a partitioned table), from some other publishers. This check is
* required only if "copy_data = true" and "origin = none" for CREATE
* SUBSCRIPTION and ALTER SUBSCRIPTION ... REFRESH statements to notify the
* user that data having origin might have been copied.
* required in the following scenarios:
*
* This check need not be performed on the tables that are already added
* because incremental sync for those tables will happen through WAL and the
* origin of the data can be identified from the WAL records.
* 1) For CREATE SUBSCRIPTION and ALTER SUBSCRIPTION ... REFRESH statements
* with "copy_data = true" and "origin = none":
* - Warn the user that data with an origin might have been copied.
* - This check is skipped for tables already added, as incremental sync via
* WAL allows origin tracking. The list of such tables is in
* subrel_local_oids.
*
* subrel_local_oids contains the list of relation oids that are already
* present on the subscriber.
* 2) For CREATE SUBSCRIPTION and ALTER SUBSCRIPTION ... REFRESH statements
* with "retain_dead_tuples = true" and "origin = any", and for ALTER
* SUBSCRIPTION statements that modify retain_dead_tuples or origin, or
* when the publisher's status changes (e.g., due to a connection string
* update):
* - Warn the user that only conflict detection info for local changes on
* the publisher is retained. Data from other origins may lack sufficient
* details for reliable conflict detection.
* - See comments atop worker.c for more details.
*/
static void
check_publications_origin(WalReceiverConn *wrconn, List *publications,
bool copydata, char *origin, Oid *subrel_local_oids,
bool copydata, bool retain_dead_tuples,
char *origin, Oid *subrel_local_oids,
int subrel_count, char *subname)
{
WalRcvExecResult *res;
@@ -2108,9 +2275,29 @@ check_publications_origin(WalReceiverConn *wrconn, List *publications,
Oid tableRow[1] = {TEXTOID};
List *publist = NIL;
int i;
bool check_rdt;
bool check_table_sync;
bool origin_none = origin &&
pg_strcasecmp(origin, LOGICALREP_ORIGIN_NONE) == 0;
if (!copydata || !origin ||
(pg_strcasecmp(origin, LOGICALREP_ORIGIN_NONE) != 0))
/*
* Enable retain_dead_tuples checks only when origin is set to 'any',
* since with origin='none' only local changes are replicated to the
* subscriber.
*/
check_rdt = retain_dead_tuples && !origin_none;
/*
* Enable table synchronization checks only when origin is 'none', to
* ensure that data from other origins is not inadvertently copied.
*/
check_table_sync = copydata && origin_none;
/* retain_dead_tuples and table sync checks occur separately */
Assert(!(check_rdt && check_table_sync));
/* Return if no checks are required */
if (!check_rdt && !check_table_sync)
return;
initStringInfo(&cmd);
@@ -2129,16 +2316,23 @@ check_publications_origin(WalReceiverConn *wrconn, List *publications,
/*
* In case of ALTER SUBSCRIPTION ... REFRESH, subrel_local_oids contains
* the list of relation oids that are already present on the subscriber.
* This check should be skipped for these tables.
* This check should be skipped for these tables if checking for table
* sync scenario. However, when handling the retain_dead_tuples scenario,
* ensure all tables are checked, as some existing tables may now include
* changes from other origins due to newly created subscriptions on the
* publisher.
*/
for (i = 0; i < subrel_count; i++)
if (check_table_sync)
{
Oid relid = subrel_local_oids[i];
char *schemaname = get_namespace_name(get_rel_namespace(relid));
char *tablename = get_rel_name(relid);
for (i = 0; i < subrel_count; i++)
{
Oid relid = subrel_local_oids[i];
char *schemaname = get_namespace_name(get_rel_namespace(relid));
char *tablename = get_rel_name(relid);
appendStringInfo(&cmd, "AND NOT (N.nspname = '%s' AND C.relname = '%s')\n",
schemaname, tablename);
appendStringInfo(&cmd, "AND NOT (N.nspname = '%s' AND C.relname = '%s')\n",
schemaname, tablename);
}
}
res = walrcv_exec(wrconn, cmd.data, 1, tableRow);
@@ -2173,22 +2367,37 @@ check_publications_origin(WalReceiverConn *wrconn, List *publications,
* XXX: For simplicity, we don't check whether the table has any data or
* not. If the table doesn't have any data then we don't need to
* distinguish between data having origin and data not having origin so we
* can avoid logging a warning in that case.
* can avoid logging a warning for table sync scenario.
*/
if (publist)
{
StringInfo pubnames = makeStringInfo();
StringInfo err_msg = makeStringInfo();
StringInfo err_hint = makeStringInfo();
/* Prepare the list of publication(s) for warning message. */
GetPublicationsStr(publist, pubnames, false);
if (check_table_sync)
{
appendStringInfo(err_msg, _("subscription \"%s\" requested copy_data with origin = NONE but might copy data that had a different origin"),
subname);
appendStringInfoString(err_hint, _("Verify that initial data copied from the publisher tables did not come from other origins."));
}
else
{
appendStringInfo(err_msg, _("subscription \"%s\" enabled retain_dead_tuples but might not reliably detect conflicts for changes from different origins"),
subname);
appendStringInfoString(err_hint, _("Consider using origin = NONE or disabling retain_dead_tuples."));
}
ereport(WARNING,
errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("subscription \"%s\" requested copy_data with origin = NONE but might copy data that had a different origin",
subname),
errdetail_plural("The subscription being created subscribes to a publication (%s) that contains tables that are written to by other subscriptions.",
"The subscription being created subscribes to publications (%s) that contain tables that are written to by other subscriptions.",
errmsg_internal("%s", err_msg->data),
errdetail_plural("The subscription subscribes to a publication (%s) that contains tables that are written to by other subscriptions.",
"The subscription subscribes to publications (%s) that contain tables that are written to by other subscriptions.",
list_length(publist), pubnames->data),
errhint("Verify that initial data copied from the publisher tables did not come from other origins."));
errhint_internal("%s", err_hint->data));
}
ExecDropSingleTupleTableSlot(slot);
@@ -2196,6 +2405,101 @@ check_publications_origin(WalReceiverConn *wrconn, List *publications,
walrcv_clear_result(res);
}
/*
* Determine whether the retain_dead_tuples can be enabled based on the
* publisher's status.
*
* This option is disallowed if the publisher is running a version earlier
* than the PG19, or if the publisher is in recovery (i.e., it is a standby
* server).
*
* See comments atop worker.c for a detailed explanation.
*/
static void
check_pub_dead_tuple_retention(WalReceiverConn *wrconn)
{
WalRcvExecResult *res;
Oid RecoveryRow[1] = {BOOLOID};
TupleTableSlot *slot;
bool isnull;
bool remote_in_recovery;
if (walrcv_server_version(wrconn) < 19000)
ereport(ERROR,
errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot enable retain_dead_tuples if the publisher is running a version earlier than PostgreSQL 19"));
res = walrcv_exec(wrconn, "SELECT pg_is_in_recovery()", 1, RecoveryRow);
if (res->status != WALRCV_OK_TUPLES)
ereport(ERROR,
(errcode(ERRCODE_CONNECTION_FAILURE),
errmsg("could not obtain recovery progress from the publisher: %s",
res->err)));
slot = MakeSingleTupleTableSlot(res->tupledesc, &TTSOpsMinimalTuple);
if (!tuplestore_gettupleslot(res->tuplestore, true, false, slot))
elog(ERROR, "failed to fetch tuple for the recovery progress");
remote_in_recovery = DatumGetBool(slot_getattr(slot, 1, &isnull));
if (remote_in_recovery)
ereport(ERROR,
errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("cannot enable retain_dead_tuples if the publisher is in recovery."));
ExecDropSingleTupleTableSlot(slot);
walrcv_clear_result(res);
}
/*
* Check if the subscriber's configuration is adequate to enable the
* retain_dead_tuples option.
*
* Issue an ERROR if the wal_level does not support the use of replication
* slots when check_guc is set to true.
*
* Issue a WARNING if track_commit_timestamp is not enabled when check_guc is
* set to true. This is only to highlight the importance of enabling
* track_commit_timestamp instead of catching all the misconfigurations, as
* this setting can be adjusted after subscription creation. Without it, the
* apply worker will simply skip conflict detection.
*
* Issue a WARNING or NOTICE if the subscription is disabled. Do not raise an
* ERROR since users can only modify retain_dead_tuples for disabled
* subscriptions. And as long as the subscription is enabled promptly, it will
* not pose issues.
*/
void
CheckSubDeadTupleRetention(bool check_guc, bool sub_disabled,
int elevel_for_sub_disabled)
{
Assert(elevel_for_sub_disabled == NOTICE ||
elevel_for_sub_disabled == WARNING);
if (check_guc && wal_level < WAL_LEVEL_REPLICA)
ereport(ERROR,
errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("\"wal_level\" is insufficient to create the replication slot required by retain_dead_tuples"),
errhint("\"wal_level\" must be set to \"replica\" or \"logical\" at server start."));
if (check_guc && !track_commit_timestamp)
ereport(WARNING,
errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("commit timestamp and origin data required for detecting conflicts won't be retained"),
errhint("Consider setting \"%s\" to true.",
"track_commit_timestamp"));
if (sub_disabled)
ereport(elevel_for_sub_disabled,
errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("deleted rows to detect conflicts would not be removed until the subscription is enabled"),
(elevel_for_sub_disabled > NOTICE)
? errhint("Consider setting %s to false.",
"retain_dead_tuples") : 0);
}
/*
* Get the list of tables which belong to specified publications on the
* publisher connection.