aboutsummaryrefslogtreecommitdiff
path: root/src/backend/executor/nodeModifyTable.c
diff options
context:
space:
mode:
authorNoah Misch <noah@leadboat.com>2024-09-24 15:25:18 -0700
committerNoah Misch <noah@leadboat.com>2024-09-24 15:25:23 -0700
commit5c837f8fa022ff9d9ebced71fdcae273fe433570 (patch)
tree95a912c65ea37cc8fab2ecab83619ba7575df11c /src/backend/executor/nodeModifyTable.c
parent8590c942c1a6b861d0cf4fa5aa694ab3a65fa306 (diff)
downloadpostgresql-5c837f8fa022ff9d9ebced71fdcae273fe433570.tar.gz
postgresql-5c837f8fa022ff9d9ebced71fdcae273fe433570.zip
For inplace update durability, make heap_update() callers wait.
The previous commit fixed some ways of losing an inplace update. It remained possible to lose one when a backend working toward a heap_update() copied a tuple into memory just before inplace update of that tuple. In catalogs eligible for inplace update, use LOCKTAG_TUPLE to govern admission to the steps of copying an old tuple, modifying it, and issuing heap_update(). This includes MERGE commands. To avoid changing most of the pg_class DDL, don't require LOCKTAG_TUPLE when holding a relation lock sufficient to exclude inplace updaters. Back-patch to v12 (all supported versions). In v13 and v12, "UPDATE pg_class" or "UPDATE pg_database" can still lose an inplace update. The v14+ UPDATE fix needs commit 86dc90056dfdbd9d1b891718d2e5614e3e432f35, and it wasn't worth reimplementing that fix without such infrastructure. Reviewed by Nitin Motiani and (in earlier versions) Heikki Linnakangas. Discussion: https://postgr.es/m/20231027214946.79.nmisch@google.com
Diffstat (limited to 'src/backend/executor/nodeModifyTable.c')
-rw-r--r--src/backend/executor/nodeModifyTable.c87
1 files changed, 76 insertions, 11 deletions
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 13849fd2348..bcb46c8e781 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -2307,6 +2307,8 @@ ExecUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
}
else
{
+ ItemPointerData lockedtid;
+
/*
* If we generate a new candidate tuple after EvalPlanQual testing, we
* must loop back here to try again. (We don't need to redo triggers,
@@ -2315,6 +2317,7 @@ ExecUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
* to do them again.)
*/
redo_act:
+ lockedtid = *tupleid;
result = ExecUpdateAct(context, resultRelInfo, tupleid, oldtuple, slot,
canSetTag, &updateCxt);
@@ -2408,6 +2411,14 @@ redo_act:
ExecInitUpdateProjection(context->mtstate,
resultRelInfo);
+ if (resultRelInfo->ri_needLockTagTuple)
+ {
+ UnlockTuple(resultRelationDesc,
+ &lockedtid, InplaceUpdateTupleLock);
+ LockTuple(resultRelationDesc,
+ tupleid, InplaceUpdateTupleLock);
+ }
+
/* Fetch the most recent version of old tuple. */
oldSlot = resultRelInfo->ri_oldTupleSlot;
if (!table_tuple_fetch_row_version(resultRelationDesc,
@@ -2512,6 +2523,14 @@ ExecOnConflictUpdate(ModifyTableContext *context,
TransactionId xmin;
bool isnull;
+ /*
+ * Parse analysis should have blocked ON CONFLICT for all system
+ * relations, which includes these. There's no fundamental obstacle to
+ * supporting this; we'd just need to handle LOCKTAG_TUPLE like the other
+ * ExecUpdate() caller.
+ */
+ Assert(!resultRelInfo->ri_needLockTagTuple);
+
/* Determine lock mode to use */
lockmode = ExecUpdateLockMode(context->estate, resultRelInfo);
@@ -2795,18 +2814,20 @@ ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
ItemPointer tupleid, bool canSetTag)
{
ModifyTableState *mtstate = context->mtstate;
+ ItemPointerData lockedtid;
TupleTableSlot *newslot;
EState *estate = context->estate;
ExprContext *econtext = mtstate->ps.ps_ExprContext;
bool isNull;
EPQState *epqstate = &mtstate->mt_epqstate;
ListCell *l;
+ bool no_further_action = true;
/*
* If there are no WHEN MATCHED actions, we are done.
*/
if (resultRelInfo->ri_matchedMergeAction == NIL)
- return true;
+ goto out;
/*
* Make tuple and any needed join variables available to ExecQual and
@@ -2820,6 +2841,20 @@ ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
lmerge_matched:;
+ if (resultRelInfo->ri_needLockTagTuple)
+ {
+ /*
+ * This locks even for CMD_DELETE, for CMD_NOTHING, and for tuples
+ * that don't match mas_whenqual. MERGE on system catalogs is a minor
+ * use case, so don't bother optimizing those.
+ */
+ LockTuple(resultRelInfo->ri_RelationDesc, tupleid,
+ InplaceUpdateTupleLock);
+ lockedtid = *tupleid;
+ }
+ else
+ ItemPointerSetInvalid(&lockedtid);
+
/*
* This routine is only invoked for matched rows, and we must have found
* the tupleid of the target row in that case; fetch that tuple.
@@ -2890,7 +2925,7 @@ lmerge_matched:;
tupleid, NULL, newslot, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ goto out; /* "do nothing" */
break; /* concurrent update/delete */
}
result = ExecUpdateAct(context, resultRelInfo, tupleid, NULL,
@@ -2906,7 +2941,7 @@ lmerge_matched:;
if (updateCxt.crossPartUpdate)
{
mtstate->mt_merge_updated += 1;
- return true;
+ goto out;
}
if (result == TM_Ok && updateCxt.updated)
@@ -2923,7 +2958,7 @@ lmerge_matched:;
NULL, NULL, &result))
{
if (result == TM_Ok)
- return true; /* "do nothing" */
+ goto out; /* "do nothing" */
break; /* concurrent update/delete */
}
result = ExecDeleteAct(context, resultRelInfo, tupleid, false);
@@ -3001,7 +3036,8 @@ lmerge_matched:;
* If the tuple was already deleted, return to let caller
* handle it under NOT MATCHED clauses.
*/
- return false;
+ no_further_action = false;
+ goto out;
case TM_Updated:
{
@@ -3047,13 +3083,19 @@ lmerge_matched:;
* NOT MATCHED actions.
*/
if (TupIsNull(epqslot))
- return false;
+ {
+ no_further_action = false;
+ goto out;
+ }
(void) ExecGetJunkAttribute(epqslot,
resultRelInfo->ri_RowIdAttNo,
&isNull);
if (isNull)
- return false;
+ {
+ no_further_action = false;
+ goto out;
+ }
/*
* When a tuple was updated and migrated to
@@ -3079,6 +3121,10 @@ lmerge_matched:;
* Update tupleid to that of the new tuple, for
* the refetch we do at the top.
*/
+ if (resultRelInfo->ri_needLockTagTuple)
+ UnlockTuple(resultRelInfo->ri_RelationDesc,
+ &lockedtid,
+ InplaceUpdateTupleLock);
ItemPointerCopy(&context->tmfd.ctid, tupleid);
goto lmerge_matched;
@@ -3088,7 +3134,8 @@ lmerge_matched:;
* tuple already deleted; tell caller to run NOT
* MATCHED actions
*/
- return false;
+ no_further_action = false;
+ goto out;
case TM_SelfModified:
@@ -3116,13 +3163,15 @@ lmerge_matched:;
/* This shouldn't happen */
elog(ERROR, "attempted to update or delete invisible tuple");
- return false;
+ no_further_action = false;
+ goto out;
default:
/* see table_tuple_lock call in ExecDelete() */
elog(ERROR, "unexpected table_tuple_lock status: %u",
result);
- return false;
+ no_further_action = false;
+ goto out;
}
}
@@ -3144,7 +3193,11 @@ lmerge_matched:;
/*
* Successfully executed an action or no qualifying action was found.
*/
- return true;
+out:
+ if (ItemPointerIsValid(&lockedtid))
+ UnlockTuple(resultRelInfo->ri_RelationDesc, &lockedtid,
+ InplaceUpdateTupleLock);
+ return no_further_action;
}
/*
@@ -3590,6 +3643,7 @@ ExecModifyTable(PlanState *pstate)
HeapTupleData oldtupdata;
HeapTuple oldtuple;
ItemPointer tupleid;
+ bool tuplock;
CHECK_FOR_INTERRUPTS();
@@ -3832,6 +3886,8 @@ ExecModifyTable(PlanState *pstate)
break;
case CMD_UPDATE:
+ tuplock = false;
+
/* Initialize projection info if first time for this table */
if (unlikely(!resultRelInfo->ri_projectNewInfoValid))
ExecInitUpdateProjection(node, resultRelInfo);
@@ -3843,6 +3899,7 @@ ExecModifyTable(PlanState *pstate)
oldSlot = resultRelInfo->ri_oldTupleSlot;
if (oldtuple != NULL)
{
+ Assert(!resultRelInfo->ri_needLockTagTuple);
/* Use the wholerow junk attr as the old tuple. */
ExecForceStoreHeapTuple(oldtuple, oldSlot, false);
}
@@ -3851,6 +3908,11 @@ ExecModifyTable(PlanState *pstate)
/* Fetch the most recent version of old tuple. */
Relation relation = resultRelInfo->ri_RelationDesc;
+ if (resultRelInfo->ri_needLockTagTuple)
+ {
+ LockTuple(relation, tupleid, InplaceUpdateTupleLock);
+ tuplock = true;
+ }
if (!table_tuple_fetch_row_version(relation, tupleid,
SnapshotAny,
oldSlot))
@@ -3863,6 +3925,9 @@ ExecModifyTable(PlanState *pstate)
/* Now apply the update. */
slot = ExecUpdate(&context, resultRelInfo, tupleid, oldtuple,
slot, node->canSetTag);
+ if (tuplock)
+ UnlockTuple(resultRelInfo->ri_RelationDesc, tupleid,
+ InplaceUpdateTupleLock);
break;
case CMD_DELETE: