From 85abec97c03d7366bfbc89807418e5d1a6d99538 Mon Sep 17 00:00:00 2001 From: Hou Zhijie Date: Fri, 27 Sep 2024 10:52:35 +0800 Subject: [PATCH v4 3/4] Support the conflict detection for update_deleted This patch supports detecting update_deleted conflicts during update operations. If the target row cannot be found when applying update operations, we perform an additional scan of the table using snapshotAny. This scan aims to locate the most recently deleted row that matches the old column values from the remote update operation and has not yet been removed by VACUUM. If any such tuples are found, we report the update_deleted conflict along with the origin and transaction information that deleted the tuple. --- doc/src/sgml/logical-replication.sgml | 11 ++ doc/src/sgml/monitoring.sgml | 11 ++ src/backend/catalog/system_views.sql | 1 + src/backend/executor/execReplication.c | 155 +++++++++++++++++++++ src/backend/replication/logical/conflict.c | 22 +++ src/backend/replication/logical/worker.c | 53 ++++--- src/backend/utils/adt/pgstatfuncs.c | 10 +- src/include/catalog/pg_proc.dat | 6 +- src/include/executor/executor.h | 6 +- src/include/replication/conflict.h | 3 + src/test/regress/expected/rules.out | 3 +- 11 files changed, 254 insertions(+), 27 deletions(-) diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml index 98a7ad0c27..c44b0e8108 100644 --- a/doc/src/sgml/logical-replication.sgml +++ b/doc/src/sgml/logical-replication.sgml @@ -1637,6 +1637,17 @@ test_sub=# SELECT * FROM t1 ORDER BY id; + + update_deleted + + + The tuple to be updated was deleted by another origin. The update will + simply be skipped in this scenario. + Note that this conflict can only be detected when + is enabled. + + + delete_origin_differs diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml index 331315f8d3..e2cd5ec07f 100644 --- a/doc/src/sgml/monitoring.sgml +++ b/doc/src/sgml/monitoring.sgml @@ -2221,6 +2221,17 @@ description | Waiting for a newly initialized WAL file to reach durable storage + + + confl_update_deleted bigint + + + Number of times the tuple to be updated was deleted by another origin + during the application of changes. See + for details about this conflict. + + + confl_delete_origin_differs bigint diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 3456b821bc..2e306ceff6 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -1371,6 +1371,7 @@ CREATE VIEW pg_stat_subscription_stats AS ss.confl_update_origin_differs, ss.confl_update_exists, ss.confl_update_missing, + ss.confl_update_deleted, ss.confl_delete_origin_differs, ss.confl_delete_missing, ss.stats_reset diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c index 54025c9f15..4876366a22 100644 --- a/src/backend/executor/execReplication.c +++ b/src/backend/executor/execReplication.c @@ -14,11 +14,13 @@ #include "postgres.h" +#include "access/commit_ts.h" #include "access/genam.h" #include "access/relscan.h" #include "access/tableam.h" #include "access/transam.h" #include "access/xact.h" +#include "access/heapam.h" #include "catalog/pg_am_d.h" #include "commands/trigger.h" #include "executor/executor.h" @@ -26,6 +28,7 @@ #include "replication/conflict.h" #include "replication/logicalrelation.h" #include "storage/lmgr.h" +#include "storage/procarray.h" #include "utils/builtins.h" #include "utils/lsyscache.h" #include "utils/rel.h" @@ -474,6 +477,158 @@ retry: return found; } +/* + * If the tuple is identified as dead and was deleted by a transaction with a + * more recent commit timestamp, update the transaction ID, deletion time, and + * origin information associated with this tuple. + */ +static void +update_recent_dead_tuple_info(TupleTableSlot *scanslot, + TransactionId oldestXmin, + TransactionId *delete_xid, + TimestampTz *delete_time, + RepOriginId *delete_origin) +{ + BufferHeapTupleTableSlot *hslot; + HeapTuple tuple; + Buffer buf; + bool dead = false; + TransactionId xmax; + TimestampTz localts; + RepOriginId localorigin; + + hslot = (BufferHeapTupleTableSlot *) scanslot; + + tuple = ExecFetchSlotHeapTuple(scanslot, false, NULL); + buf = hslot->buffer; + + LockBuffer(buf, BUFFER_LOCK_SHARE); + + if (HeapTupleSatisfiesVacuum(tuple, oldestXmin, buf) == HEAPTUPLE_RECENTLY_DEAD) + dead = true; + + LockBuffer(buf, BUFFER_LOCK_UNLOCK); + + if (!dead) + return; + + xmax = HeapTupleHeaderGetUpdateXid(tuple->t_data); + if (!TransactionIdIsValid(xmax)) + return; + + /* Select the dead tuple with the most recent commit timestamp */ + if (TransactionIdGetCommitTsData(xmax, &localts, &localorigin) && + (TimestampDifferenceExceeds(*delete_time, localts, 0) || + *delete_time == 0)) + { + *delete_xid = xmax; + *delete_time = localts; + *delete_origin = localorigin; + } +} + +/* + * Searches the relation 'rel' for the most recently deleted tuple that matches + * the values in 'searchslot' and is not yet removable by VACUUM. The function + * returns the transaction ID, origin, and commit timestamp of the transaction + * that deleted this tuple. + * + * The commit timestamp of the transaction that deleted the tuple is used to + * determine whether the tuple is the most recently deleted one. + * + * 'localindexoid' should be the replica identity index used to locate the + * target row for update or deletion operations + * + * If 'localindexoid' is invalid, the function defaults to performing a full + * table scan, which can be slow on tables with a large number of rows. + * However, it is primarily used in rare conflict cases where the target row + * for an update cannot be found. + */ +bool +FindMostRecentlyDeletedTupleInfo(Relation rel, TupleTableSlot *searchslot, + Oid localindexoid, + TransactionId *delete_xid, + TimestampTz *delete_time, + RepOriginId *delete_origin) +{ + TupleTableSlot *scanslot; + TypeCacheEntry **eq; + TransactionId oldestXmin; + TupleDesc desc PG_USED_FOR_ASSERTS_ONLY = RelationGetDescr(rel); + + Assert(equalTupleDescs(desc, searchslot->tts_tupleDescriptor)); + + *delete_xid = InvalidTransactionId; + *delete_time = 0; + *delete_origin = InvalidRepOriginId; + + /* Return if the commit timestamp data is not available */ + if (!track_commit_timestamp) + return false; + + oldestXmin = GetOldestNonRemovableTransactionId(rel); + + eq = palloc0(sizeof(*eq) * searchslot->tts_tupleDescriptor->natts); + + scanslot = table_slot_create(rel, NULL); + + /* Use local index as much as possible */ + if (OidIsValid(localindexoid)) + { + Relation idxrel; + ScanKeyData skey[INDEX_MAX_KEYS]; + int skey_attoff; + IndexScanDesc scan; + + idxrel = index_open(localindexoid, RowExclusiveLock); + + /* Build scan key. */ + skey_attoff = build_replindex_scan_key(skey, rel, idxrel, searchslot); + + /* Start an index scan. */ + scan = index_beginscan(rel, idxrel, SnapshotAny, skey_attoff, 0); + + index_rescan(scan, skey, skey_attoff, NULL, 0); + + /* Try to find the tuple */ + while (index_getnext_slot(scan, ForwardScanDirection, scanslot)) + { + Assert(tuples_equal(scanslot, searchslot, eq)); + update_recent_dead_tuple_info(scanslot, oldestXmin, delete_xid, + delete_time, delete_origin); + } + + index_endscan(scan); + + index_close(idxrel, NoLock); + } + else + { + TableScanDesc scan; + + /* Start a heap scan. */ + scan = table_beginscan(rel, SnapshotAny, 0, NULL); + + table_rescan(scan, NULL); + + /* Try to find the tuple */ + while (table_scan_getnextslot(scan, ForwardScanDirection, scanslot)) + { + if (!tuples_equal(scanslot, searchslot, eq)) + continue; + + update_recent_dead_tuple_info(scanslot, oldestXmin, delete_xid, + delete_time, delete_origin); + } + + table_endscan(scan); + } + + ExecDropSingleTupleTableSlot(scanslot); + + return *delete_time != 0; +} + /* * Find the tuple that violates the passed unique index (conflictindex). * diff --git a/src/backend/replication/logical/conflict.c b/src/backend/replication/logical/conflict.c index 5d9ff626bd..cd39727932 100644 --- a/src/backend/replication/logical/conflict.c +++ b/src/backend/replication/logical/conflict.c @@ -28,6 +28,7 @@ static const char *const ConflictTypeNames[] = { [CT_UPDATE_ORIGIN_DIFFERS] = "update_origin_differs", [CT_UPDATE_EXISTS] = "update_exists", [CT_UPDATE_MISSING] = "update_missing", + [CT_UPDATE_DELETED] = "update_deleted", [CT_DELETE_ORIGIN_DIFFERS] = "delete_origin_differs", [CT_DELETE_MISSING] = "delete_missing" }; @@ -172,6 +173,7 @@ errcode_apply_conflict(ConflictType type) return errcode(ERRCODE_UNIQUE_VIOLATION); case CT_UPDATE_ORIGIN_DIFFERS: case CT_UPDATE_MISSING: + case CT_UPDATE_DELETED: case CT_DELETE_ORIGIN_DIFFERS: case CT_DELETE_MISSING: return errcode(ERRCODE_T_R_SERIALIZATION_FAILURE); @@ -259,6 +261,26 @@ errdetail_apply_conflict(EState *estate, ResultRelInfo *relinfo, appendStringInfo(&err_detail, _("Could not find the row to be updated.")); break; + case CT_UPDATE_DELETED: + if (localts) + { + if (localorigin == InvalidRepOriginId) + appendStringInfo(&err_detail, _("The row to be updated was deleted locally in transaction %u at %s."), + localxmin, timestamptz_to_str(localts)); + else if (replorigin_by_oid(localorigin, true, &origin_name)) + appendStringInfo(&err_detail, _("The row to be updated was deleted by a different origin \"%s\" in transaction %u at %s."), + origin_name, localxmin, timestamptz_to_str(localts)); + + /* The origin that modified this row has been removed. */ + else + appendStringInfo(&err_detail, _("The row to be updated was deleted by a non-existent origin in transaction %u at %s."), + localxmin, timestamptz_to_str(localts)); + } + else + appendStringInfo(&err_detail, _("The row to be updated was deleted.")); + + break; + case CT_DELETE_ORIGIN_DIFFERS: if (localorigin == InvalidRepOriginId) appendStringInfo(&err_detail, _("Deleting the row that was modified locally in transaction %u at %s."), diff --git a/src/backend/replication/logical/worker.c b/src/backend/replication/logical/worker.c index d61da33e3f..247f62b31c 100644 --- a/src/backend/replication/logical/worker.c +++ b/src/backend/replication/logical/worker.c @@ -2688,6 +2688,9 @@ apply_handle_update_internal(ApplyExecutionData *edata, TupleTableSlot *localslot; bool found; MemoryContext oldctx; + RepOriginId localorigin; + TransactionId localxid; + TimestampTz localts; EvalPlanQualInit(&epqstate, estate, NULL, NIL, -1, NIL); ExecOpenIndices(relinfo, true); @@ -2704,15 +2707,11 @@ apply_handle_update_internal(ApplyExecutionData *edata, */ if (found) { - RepOriginId localorigin; - TransactionId localxmin; - TimestampTz localts; - /* * Report the conflict if the tuple was modified by a different * origin. */ - if (GetTupleTransactionInfo(localslot, &localxmin, &localorigin, &localts) && + if (GetTupleTransactionInfo(localslot, &localxid, &localorigin, &localts) && localorigin != replorigin_session_origin) { TupleTableSlot *newslot; @@ -2723,7 +2722,7 @@ apply_handle_update_internal(ApplyExecutionData *edata, ReportApplyConflict(estate, relinfo, LOG, CT_UPDATE_ORIGIN_DIFFERS, remoteslot, localslot, newslot, - InvalidOid, localxmin, localorigin, localts); + InvalidOid, localxid, localorigin, localts); } /* Process and store remote tuple in the slot */ @@ -2742,19 +2741,28 @@ apply_handle_update_internal(ApplyExecutionData *edata, } else { + ConflictType type; TupleTableSlot *newslot = localslot; + Oid replica_index = GetRelationIdentityOrPK(localrel); + + if (FindMostRecentlyDeletedTupleInfo(localrel, remoteslot, + replica_index, &localxid, + &localts, &localorigin) && + localorigin != replorigin_session_origin) + type = CT_UPDATE_DELETED; + else + type = CT_UPDATE_MISSING; /* Store the new tuple for conflict reporting */ slot_store_data(newslot, relmapentry, newtup); /* - * The tuple to be updated could not be found. Do nothing except for - * emitting a log message. + * The tuple to be updated could not be found or was deleted. Do + * nothing except for emitting a log message. */ - ReportApplyConflict(estate, relinfo, LOG, CT_UPDATE_MISSING, + ReportApplyConflict(estate, relinfo, LOG, type, remoteslot, NULL, newslot, - InvalidOid, InvalidTransactionId, - InvalidRepOriginId, 0); + InvalidOid, localxid, localorigin, localts); } /* Cleanup. */ @@ -3076,7 +3084,7 @@ apply_handle_tuple_routing(ApplyExecutionData *edata, bool found; EPQState epqstate; RepOriginId localorigin; - TransactionId localxmin; + TransactionId localxid; TimestampTz localts; /* Get the matching local tuple from the partition. */ @@ -3086,17 +3094,26 @@ apply_handle_tuple_routing(ApplyExecutionData *edata, remoteslot_part, &localslot); if (!found) { + ConflictType type; TupleTableSlot *newslot = localslot; + Oid replica_index = GetRelationIdentityOrPK(partrel); + + if (FindMostRecentlyDeletedTupleInfo(partrel, remoteslot_part, + replica_index, &localxid, + &localts, &localorigin) && + localorigin != replorigin_session_origin) + type = CT_UPDATE_DELETED; + else + type = CT_UPDATE_MISSING; /* Store the new tuple for conflict reporting */ slot_store_data(newslot, part_entry, newtup); /* - * The tuple to be updated could not be found. Do nothing - * except for emitting a log message. + * The tuple to be updated could not be found or was + * deleted. Do nothing except for emitting a log message. */ - ReportApplyConflict(estate, partrelinfo, - LOG, CT_UPDATE_MISSING, + ReportApplyConflict(estate, partrelinfo, LOG, type, remoteslot_part, NULL, newslot, InvalidOid, InvalidTransactionId, InvalidRepOriginId, 0); @@ -3108,7 +3125,7 @@ apply_handle_tuple_routing(ApplyExecutionData *edata, * Report the conflict if the tuple was modified by a * different origin. */ - if (GetTupleTransactionInfo(localslot, &localxmin, &localorigin, &localts) && + if (GetTupleTransactionInfo(localslot, &localxid, &localorigin, &localts) && localorigin != replorigin_session_origin) { TupleTableSlot *newslot; @@ -3119,7 +3136,7 @@ apply_handle_tuple_routing(ApplyExecutionData *edata, ReportApplyConflict(estate, partrelinfo, LOG, CT_UPDATE_ORIGIN_DIFFERS, remoteslot_part, localslot, newslot, - InvalidOid, localxmin, localorigin, + InvalidOid, localxid, localorigin, localts); } diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c index f7b50e0b5a..0a3353eb5c 100644 --- a/src/backend/utils/adt/pgstatfuncs.c +++ b/src/backend/utils/adt/pgstatfuncs.c @@ -1978,7 +1978,7 @@ pg_stat_get_replication_slot(PG_FUNCTION_ARGS) Datum pg_stat_get_subscription_stats(PG_FUNCTION_ARGS) { -#define PG_STAT_GET_SUBSCRIPTION_STATS_COLS 10 +#define PG_STAT_GET_SUBSCRIPTION_STATS_COLS 11 Oid subid = PG_GETARG_OID(0); TupleDesc tupdesc; Datum values[PG_STAT_GET_SUBSCRIPTION_STATS_COLS] = {0}; @@ -2006,11 +2006,13 @@ pg_stat_get_subscription_stats(PG_FUNCTION_ARGS) INT8OID, -1, 0); TupleDescInitEntry(tupdesc, (AttrNumber) 7, "confl_update_missing", INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 8, "confl_delete_origin_differs", + TupleDescInitEntry(tupdesc, (AttrNumber) 8, "confl_update_deleted", INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 9, "confl_delete_missing", + TupleDescInitEntry(tupdesc, (AttrNumber) 9, "confl_delete_origin_differs", INT8OID, -1, 0); - TupleDescInitEntry(tupdesc, (AttrNumber) 10, "stats_reset", + TupleDescInitEntry(tupdesc, (AttrNumber) 10, "confl_delete_missing", + INT8OID, -1, 0); + TupleDescInitEntry(tupdesc, (AttrNumber) 11, "stats_reset", TIMESTAMPTZOID, -1, 0); BlessTupleDesc(tupdesc); diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 3ae31a614c..b8d9c15d47 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -5597,9 +5597,9 @@ { oid => '6231', descr => 'statistics: information about subscription stats', proname => 'pg_stat_get_subscription_stats', provolatile => 's', proparallel => 'r', prorettype => 'record', proargtypes => 'oid', - proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,timestamptz}', - proargmodes => '{i,o,o,o,o,o,o,o,o,o,o}', - proargnames => '{subid,subid,apply_error_count,sync_error_count,confl_insert_exists,confl_update_origin_differs,confl_update_exists,confl_update_missing,confl_delete_origin_differs,confl_delete_missing,stats_reset}', + proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,timestamptz}', + proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{subid,subid,apply_error_count,sync_error_count,confl_insert_exists,confl_update_origin_differs,confl_update_exists,confl_update_missing,confl_update_deleted,confl_delete_origin_differs,confl_delete_missing,stats_reset}', prosrc => 'pg_stat_get_subscription_stats' }, { oid => '6118', descr => 'statistics: information about subscription', proname => 'pg_stat_get_subscription', prorows => '10', proisstrict => 'f', diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h index 69c3ebff00..bb04ef86c2 100644 --- a/src/include/executor/executor.h +++ b/src/include/executor/executor.h @@ -665,7 +665,11 @@ extern bool RelationFindReplTupleByIndex(Relation rel, Oid idxoid, TupleTableSlot *outslot); extern bool RelationFindReplTupleSeq(Relation rel, LockTupleMode lockmode, TupleTableSlot *searchslot, TupleTableSlot *outslot); - +extern bool FindMostRecentlyDeletedTupleInfo(Relation rel, TupleTableSlot *searchslot, + Oid localindexoid, + TransactionId *delete_xid, + TimestampTz *delete_time, + RepOriginId *delete_origin); extern void ExecSimpleRelationInsert(ResultRelInfo *resultRelInfo, EState *estate, TupleTableSlot *slot); extern void ExecSimpleRelationUpdate(ResultRelInfo *resultRelInfo, diff --git a/src/include/replication/conflict.h b/src/include/replication/conflict.h index c759677ff5..d2d3e2f064 100644 --- a/src/include/replication/conflict.h +++ b/src/include/replication/conflict.h @@ -35,6 +35,9 @@ typedef enum /* The row to be updated is missing */ CT_UPDATE_MISSING, + /* The row to be updated is deleted */ + CT_UPDATE_DELETED, + /* The row to be deleted was modified by a different origin */ CT_DELETE_ORIGIN_DIFFERS, diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 2b47013f11..4dad6e4966 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -2145,11 +2145,12 @@ pg_stat_subscription_stats| SELECT ss.subid, ss.confl_update_origin_differs, ss.confl_update_exists, ss.confl_update_missing, + ss.confl_update_deleted, ss.confl_delete_origin_differs, ss.confl_delete_missing, ss.stats_reset FROM pg_subscription s, - LATERAL pg_stat_get_subscription_stats(s.oid) ss(subid, apply_error_count, sync_error_count, confl_insert_exists, confl_update_origin_differs, confl_update_exists, confl_update_missing, confl_delete_origin_differs, confl_delete_missing, stats_reset); + LATERAL pg_stat_get_subscription_stats(s.oid) ss(subid, apply_error_count, sync_error_count, confl_insert_exists, confl_update_origin_differs, confl_update_exists, confl_update_missing, confl_update_deleted, confl_delete_origin_differs, confl_delete_missing, stats_reset); pg_stat_sys_indexes| SELECT relid, indexrelid, schemaname, -- 2.30.0.windows.2