mirror of
https://github.com/postgres/postgres.git
synced 2025-06-27 23:21:58 +03:00
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 86dc90056d
,
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
This commit is contained in:
@ -2321,6 +2321,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,
|
||||
@ -2329,6 +2331,7 @@ ExecUpdate(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
|
||||
* to do them again.)
|
||||
*/
|
||||
redo_act:
|
||||
lockedtid = *tupleid;
|
||||
result = ExecUpdateAct(context, resultRelInfo, tupleid, oldtuple, slot,
|
||||
canSetTag, &updateCxt);
|
||||
|
||||
@ -2422,6 +2425,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,
|
||||
@ -2526,6 +2537,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);
|
||||
|
||||
@ -2851,6 +2870,7 @@ ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
|
||||
{
|
||||
ModifyTableState *mtstate = context->mtstate;
|
||||
List **mergeActions = resultRelInfo->ri_MergeActions;
|
||||
ItemPointerData lockedtid;
|
||||
List *actionStates;
|
||||
TupleTableSlot *newslot = NULL;
|
||||
TupleTableSlot *rslot = NULL;
|
||||
@ -2887,14 +2907,32 @@ ExecMergeMatched(ModifyTableContext *context, ResultRelInfo *resultRelInfo,
|
||||
* target wholerow junk attr.
|
||||
*/
|
||||
Assert(tupleid != NULL || oldtuple != NULL);
|
||||
ItemPointerSetInvalid(&lockedtid);
|
||||
if (oldtuple != NULL)
|
||||
{
|
||||
Assert(!resultRelInfo->ri_needLockTagTuple);
|
||||
ExecForceStoreHeapTuple(oldtuple, resultRelInfo->ri_oldTupleSlot,
|
||||
false);
|
||||
else if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc,
|
||||
tupleid,
|
||||
SnapshotAny,
|
||||
resultRelInfo->ri_oldTupleSlot))
|
||||
elog(ERROR, "failed to fetch the target tuple");
|
||||
}
|
||||
else
|
||||
{
|
||||
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;
|
||||
}
|
||||
if (!table_tuple_fetch_row_version(resultRelInfo->ri_RelationDesc,
|
||||
tupleid,
|
||||
SnapshotAny,
|
||||
resultRelInfo->ri_oldTupleSlot))
|
||||
elog(ERROR, "failed to fetch the target tuple");
|
||||
}
|
||||
|
||||
/*
|
||||
* Test the join condition. If it's satisfied, perform a MATCHED action.
|
||||
@ -2966,7 +3004,7 @@ lmerge_matched:
|
||||
tupleid, NULL, newslot, &result))
|
||||
{
|
||||
if (result == TM_Ok)
|
||||
return NULL; /* "do nothing" */
|
||||
goto out; /* "do nothing" */
|
||||
|
||||
break; /* concurrent update/delete */
|
||||
}
|
||||
@ -2977,11 +3015,11 @@ lmerge_matched:
|
||||
{
|
||||
if (!ExecIRUpdateTriggers(estate, resultRelInfo,
|
||||
oldtuple, newslot))
|
||||
return NULL; /* "do nothing" */
|
||||
goto out; /* "do nothing" */
|
||||
}
|
||||
else
|
||||
{
|
||||
/* called table_tuple_fetch_row_version() above */
|
||||
/* checked ri_needLockTagTuple above */
|
||||
Assert(oldtuple == NULL);
|
||||
|
||||
result = ExecUpdateAct(context, resultRelInfo, tupleid,
|
||||
@ -3000,7 +3038,8 @@ lmerge_matched:
|
||||
if (updateCxt.crossPartUpdate)
|
||||
{
|
||||
mtstate->mt_merge_updated += 1;
|
||||
return context->cpUpdateReturningSlot;
|
||||
rslot = context->cpUpdateReturningSlot;
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3018,7 +3057,7 @@ lmerge_matched:
|
||||
NULL, NULL, &result))
|
||||
{
|
||||
if (result == TM_Ok)
|
||||
return NULL; /* "do nothing" */
|
||||
goto out; /* "do nothing" */
|
||||
|
||||
break; /* concurrent update/delete */
|
||||
}
|
||||
@ -3029,11 +3068,11 @@ lmerge_matched:
|
||||
{
|
||||
if (!ExecIRDeleteTriggers(estate, resultRelInfo,
|
||||
oldtuple))
|
||||
return NULL; /* "do nothing" */
|
||||
goto out; /* "do nothing" */
|
||||
}
|
||||
else
|
||||
{
|
||||
/* called table_tuple_fetch_row_version() above */
|
||||
/* checked ri_needLockTagTuple above */
|
||||
Assert(oldtuple == NULL);
|
||||
|
||||
result = ExecDeleteAct(context, resultRelInfo, tupleid,
|
||||
@ -3115,7 +3154,7 @@ lmerge_matched:
|
||||
* let caller handle it under NOT MATCHED [BY TARGET] clauses.
|
||||
*/
|
||||
*matched = false;
|
||||
return NULL;
|
||||
goto out;
|
||||
|
||||
case TM_Updated:
|
||||
{
|
||||
@ -3189,7 +3228,7 @@ lmerge_matched:
|
||||
* more to do.
|
||||
*/
|
||||
if (TupIsNull(epqslot))
|
||||
return NULL;
|
||||
goto out;
|
||||
|
||||
/*
|
||||
* If we got a NULL ctid from the subplan, the
|
||||
@ -3207,6 +3246,15 @@ lmerge_matched:
|
||||
* we need to switch to the NOT MATCHED BY
|
||||
* SOURCE case.
|
||||
*/
|
||||
if (resultRelInfo->ri_needLockTagTuple)
|
||||
{
|
||||
if (ItemPointerIsValid(&lockedtid))
|
||||
UnlockTuple(resultRelInfo->ri_RelationDesc, &lockedtid,
|
||||
InplaceUpdateTupleLock);
|
||||
LockTuple(resultRelInfo->ri_RelationDesc, &context->tmfd.ctid,
|
||||
InplaceUpdateTupleLock);
|
||||
lockedtid = context->tmfd.ctid;
|
||||
}
|
||||
if (!table_tuple_fetch_row_version(resultRelationDesc,
|
||||
&context->tmfd.ctid,
|
||||
SnapshotAny,
|
||||
@ -3235,7 +3283,7 @@ lmerge_matched:
|
||||
* MATCHED [BY TARGET] actions
|
||||
*/
|
||||
*matched = false;
|
||||
return NULL;
|
||||
goto out;
|
||||
|
||||
case TM_SelfModified:
|
||||
|
||||
@ -3263,13 +3311,13 @@ lmerge_matched:
|
||||
|
||||
/* This shouldn't happen */
|
||||
elog(ERROR, "attempted to update or delete invisible tuple");
|
||||
return NULL;
|
||||
goto out;
|
||||
|
||||
default:
|
||||
/* see table_tuple_lock call in ExecDelete() */
|
||||
elog(ERROR, "unexpected table_tuple_lock status: %u",
|
||||
result);
|
||||
return NULL;
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3316,6 +3364,10 @@ lmerge_matched:
|
||||
/*
|
||||
* Successfully executed an action or no qualifying action was found.
|
||||
*/
|
||||
out:
|
||||
if (ItemPointerIsValid(&lockedtid))
|
||||
UnlockTuple(resultRelInfo->ri_RelationDesc, &lockedtid,
|
||||
InplaceUpdateTupleLock);
|
||||
return rslot;
|
||||
}
|
||||
|
||||
@ -3767,6 +3819,7 @@ ExecModifyTable(PlanState *pstate)
|
||||
HeapTupleData oldtupdata;
|
||||
HeapTuple oldtuple;
|
||||
ItemPointer tupleid;
|
||||
bool tuplock;
|
||||
|
||||
CHECK_FOR_INTERRUPTS();
|
||||
|
||||
@ -4079,6 +4132,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);
|
||||
@ -4090,6 +4145,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);
|
||||
}
|
||||
@ -4098,6 +4154,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))
|
||||
@ -4109,6 +4170,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:
|
||||
|
Reference in New Issue
Block a user