diff options
author | Noah Misch <noah@leadboat.com> | 2024-09-24 15:25:18 -0700 |
---|---|---|
committer | Noah Misch <noah@leadboat.com> | 2024-09-24 15:25:23 -0700 |
commit | 5c837f8fa022ff9d9ebced71fdcae273fe433570 (patch) | |
tree | 95a912c65ea37cc8fab2ecab83619ba7575df11c /src/backend/executor/nodeModifyTable.c | |
parent | 8590c942c1a6b861d0cf4fa5aa694ab3a65fa306 (diff) | |
download | postgresql-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.c | 87 |
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: |