1
0
mirror of https://github.com/postgres/postgres.git synced 2025-08-28 18:48:04 +03:00

Fix concurrent update issues with MERGE.

If MERGE attempts an UPDATE or DELETE on a table with BEFORE ROW
triggers, or a cross-partition UPDATE (with or without triggers), and
a concurrent UPDATE or DELETE happens, the merge code would fail.

In some cases this would lead to a crash, while in others it would
cause the wrong merge action to be executed, or no action at all. The
immediate cause of the crash was the trigger code calling
ExecGetUpdateNewTuple() as part of the EPQ mechanism, which fails
because during a merge ri_projectNew is NULL, since merge has its own
per-action projection information, which ExecGetUpdateNewTuple() knows
nothing about.

Fix by arranging for the trigger code to exit early, returning the
TM_Result and TM_FailureData information, if a concurrent modification
is detected, allowing the merge code to do the necessary EPQ handling
in its own way. Similarly, prevent the cross-partition update code
from doing any EPQ processing for a merge, allowing the merge code to
work out what it needs to do.

This leads to a number of simplifications in nodeModifyTable.c. Most
notably, the ModifyTableContext->GetUpdateNewTuple() callback is no
longer needed, and mergeGetUpdateNewTuple() can be deleted, since
there is no longer any requirement for get-update-new-tuple during a
merge. Similarly, ModifyTableContext->cpUpdateRetrySlot is no longer
needed. Thus ExecGetUpdateNewTuple() and the retry_slot handling of
ExecCrossPartitionUpdate() can be restored to how they were in v14,
before the merge code was added, and ExecMergeMatched() no longer
needs any special-case handling for cross-partition updates.

While at it, tidy up ExecUpdateEpilogue() a bit, making it handle
recheckIndexes locally, rather than passing it in as a parameter,
ensuring that it is freed properly. This dates back to when it was
split off from ExecUpdate() to support merge.

Per bug #17809 from Alexander Lakhin, and follow-up investigation of
bug #17792, also from Alexander Lakhin.

Back-patch to v15, where MERGE was introduced, taking care to preserve
backwards-compatibility of the trigger API in v15 for any extensions
that might use it.

Discussion:
  https://postgr.es/m/17809-9e6650bef133f0fe%40postgresql.org
  https://postgr.es/m/17792-0f89452029662c36%40postgresql.org
This commit is contained in:
Dean Rasheed
2023-03-13 10:23:42 +00:00
parent 4493256c5c
commit 7d9a75713a
7 changed files with 695 additions and 189 deletions

View File

@@ -84,8 +84,9 @@ static bool GetTupleForTrigger(EState *estate,
ItemPointer tid,
LockTupleMode lockmode,
TupleTableSlot *oldslot,
TupleTableSlot **newSlot,
TM_FailureData *tmfpd);
TupleTableSlot **epqslot,
TM_Result *tmresultp,
TM_FailureData *tmfdp);
static bool TriggerEnabled(EState *estate, ResultRelInfo *relinfo,
Trigger *trigger, TriggerEvent event,
Bitmapset *modifiedCols,
@@ -2753,11 +2754,13 @@ ExecASDeleteTriggers(EState *estate, ResultRelInfo *relinfo,
* back the concurrently updated tuple if any.
*/
bool
ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot)
ExecBRDeleteTriggersNew(EState *estate, EPQState *epqstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot,
TM_Result *tmresult,
TM_FailureData *tmfd)
{
TupleTableSlot *slot = ExecGetTriggerOldSlot(estate, relinfo);
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
@@ -2774,7 +2777,7 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
if (!GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
LockTupleExclusive, slot, &epqslot_candidate,
NULL))
tmresult, tmfd))
return false;
/*
@@ -2837,6 +2840,21 @@ ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
return result;
}
/*
* ABI-compatible wrapper to emulate old version of the above function.
* Do not call this version in new code.
*/
bool
ExecBRDeleteTriggers(EState *estate, EPQState *epqstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot **epqslot)
{
return ExecBRDeleteTriggersNew(estate, epqstate, relinfo, tupleid,
fdw_trigtuple, epqslot, NULL, NULL);
}
/*
* Note: is_crosspart_update must be true if the DELETE is being performed
* as part of a cross-partition update.
@@ -2865,6 +2883,7 @@ ExecARDeleteTriggers(EState *estate,
LockTupleExclusive,
slot,
NULL,
NULL,
NULL);
else
ExecForceStoreHeapTuple(fdw_trigtuple, slot, false);
@@ -3001,12 +3020,13 @@ ExecASUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
}
bool
ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
TM_FailureData *tmfd)
ExecBRUpdateTriggersNew(EState *estate, EPQState *epqstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
TM_Result *tmresult,
TM_FailureData *tmfd)
{
TriggerDesc *trigdesc = relinfo->ri_TrigDesc;
TupleTableSlot *oldslot = ExecGetTriggerOldSlot(estate, relinfo);
@@ -3030,7 +3050,7 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
/* get a copy of the on-disk tuple we are planning to update */
if (!GetTupleForTrigger(estate, epqstate, relinfo, tupleid,
lockmode, oldslot, &epqslot_candidate,
tmfd))
tmresult, tmfd))
return false; /* cancel the update action */
/*
@@ -3134,6 +3154,22 @@ ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
return true;
}
/*
* ABI-compatible wrapper to emulate old version of the above function.
* Do not call this version in new code.
*/
bool
ExecBRUpdateTriggers(EState *estate, EPQState *epqstate,
ResultRelInfo *relinfo,
ItemPointer tupleid,
HeapTuple fdw_trigtuple,
TupleTableSlot *newslot,
TM_FailureData *tmfd)
{
return ExecBRUpdateTriggersNew(estate, epqstate, relinfo, tupleid,
fdw_trigtuple, newslot, NULL, tmfd);
}
/*
* Note: 'src_partinfo' and 'dst_partinfo', when non-NULL, refer to the source
* and destination partitions, respectively, of a cross-partition update of
@@ -3185,6 +3221,7 @@ ExecARUpdateTriggers(EState *estate, ResultRelInfo *relinfo,
LockTupleExclusive,
oldslot,
NULL,
NULL,
NULL);
else if (fdw_trigtuple != NULL)
ExecForceStoreHeapTuple(fdw_trigtuple, oldslot, false);
@@ -3340,6 +3377,7 @@ GetTupleForTrigger(EState *estate,
LockTupleMode lockmode,
TupleTableSlot *oldslot,
TupleTableSlot **epqslot,
TM_Result *tmresultp,
TM_FailureData *tmfdp)
{
Relation relation = relinfo->ri_RelationDesc;
@@ -3367,6 +3405,8 @@ GetTupleForTrigger(EState *estate,
&tmfd);
/* Let the caller know about the status of this operation */
if (tmresultp)
*tmresultp = test;
if (tmfdp)
*tmfdp = tmfd;
@@ -3394,6 +3434,18 @@ GetTupleForTrigger(EState *estate,
case TM_Ok:
if (tmfd.traversed)
{
/*
* Recheck the tuple using EPQ. For MERGE, we leave this
* to the caller (it must do additional rechecking, and
* might end up executing a different action entirely).
*/
if (estate->es_plannedstmt->commandType == CMD_MERGE)
{
if (tmresultp)
*tmresultp = TM_Updated;
return false;
}
*epqslot = EvalPlanQual(epqstate,
relation,
relinfo->ri_RangeTableIndex,