agora inbox for [email protected]help / color / mirror / Atom feed
[PATCH v56 2/3] Reserve replication slots specifically for REPACK 97+ messages / 2 participants [nested] [flat]
* [PATCH v56 2/3] Reserve replication slots specifically for REPACK @ 2026-04-01 17:54 Álvaro Herrera <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Álvaro Herrera @ 2026-04-01 17:54 UTC (permalink / raw) This allows REPACK to not interfere with other operations that use replication slots. This eases configurability. --- doc/src/sgml/config.sgml | 16 ++++ doc/src/sgml/ref/repack.sgml | 6 +- src/backend/commands/repack.c | 2 +- src/backend/commands/repack_worker.c | 7 +- src/backend/replication/logical/launcher.c | 2 +- src/backend/replication/logical/logical.c | 8 +- .../replication/logical/logicalfuncs.c | 2 +- src/backend/replication/logical/slotsync.c | 5 +- src/backend/replication/slot.c | 84 ++++++++++++------- src/backend/replication/slotfuncs.c | 19 +++-- src/backend/replication/walsender.c | 9 +- src/backend/utils/misc/guc_parameters.dat | 8 ++ src/backend/utils/misc/postgresql.conf.sample | 2 + src/include/replication/logical.h | 3 +- src/include/replication/slot.h | 5 +- 15 files changed, 116 insertions(+), 62 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 3324d2d3c49..3435732646e 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -4639,6 +4639,22 @@ restore_command = 'copy "C:\\server\\archivedir\\%f" "%p"' # Windows </listitem> </varlistentry> + <varlistentry id="guc-max-repack-replication-slots" xreflabel="max_repack_replication_slots"> + <term><varname>max_repack_replication_slots</varname> (<type>integer</type>) + <indexterm> + <primary><varname>max_repack_replication_slots</varname> configuration parameter</primary> + </indexterm> + </term> + <listitem> + <para> + Specifies the maximum number of replication slots for use of + the <command>REPACK</command> command. The default is 5. + This parameter can only be set at server start. + </para> + </listitem> + </varlistentry> + + <varlistentry id="guc-max-replication-slots" xreflabel="max_replication_slots"> <term><varname>max_replication_slots</varname> (<type>integer</type>) <indexterm> diff --git a/doc/src/sgml/ref/repack.sgml b/doc/src/sgml/ref/repack.sgml index e993dfb3108..c532a39ee07 100644 --- a/doc/src/sgml/ref/repack.sgml +++ b/doc/src/sgml/ref/repack.sgml @@ -293,9 +293,9 @@ REPACK [ ( <replaceable class="parameter">option</replaceable> [, ...] ) ] USING <listitem> <para> - The <link linkend="guc-max-replication-slots"><varname>max_replication_slots</varname></link> - configuration parameter does not allow for creation of an additional - replication slot. + The <link linkend="guc-max-repack-replication-slots"><varname>max_repack_replication_slots</varname></link> + configuration parameter does not allow for the creation of an + additional replication slot. </para> </listitem> </itemizedlist> diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 17b639b3b44..5e97fcf3818 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3357,7 +3357,7 @@ start_repack_decoding_worker(Oid relid) ereport(ERROR, errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), errmsg("out of background worker slots"), - errhint("You might need to increase \"%s\".", "max_worker_processes")); + errhint("You might need to increase \"%s\".", "max_repack_replication_slots")); decoding_worker->seg = seg; decoding_worker->error_mqh = mqh; diff --git a/src/backend/commands/repack_worker.c b/src/backend/commands/repack_worker.c index 94d034970b5..ea330310e27 100644 --- a/src/backend/commands/repack_worker.c +++ b/src/backend/commands/repack_worker.c @@ -212,7 +212,7 @@ repack_setup_logical_decoding(Oid relid) * Make sure we can use logical decoding. */ CheckSlotPermissions(); - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(true); /* * A single backend should not execute multiple REPACK commands at a time, @@ -221,8 +221,8 @@ repack_setup_logical_decoding(Oid relid) * RS_TEMPORARY so that the slot gets cleaned up on ERROR. */ snprintf(NameStr(slotname), NAMEDATALEN, "repack_%d", MyProcPid); - ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, false, - false); + ReplicationSlotCreate(NameStr(slotname), true, RS_TEMPORARY, false, true, + false, false); EnsureLogicalDecodingEnabled(); @@ -233,6 +233,7 @@ repack_setup_logical_decoding(Oid relid) ctx = CreateInitDecodingContext(REPL_PLUGIN_NAME, NIL, true, + true, InvalidXLogRecPtr, XL_ROUTINE(.page_read = read_local_xlog_page, .segment_open = wal_segment_open, diff --git a/src/backend/replication/logical/launcher.c b/src/backend/replication/logical/launcher.c index 9e75a3e04ee..7adf4dbe0d1 100644 --- a/src/backend/replication/logical/launcher.c +++ b/src/backend/replication/logical/launcher.c @@ -1575,7 +1575,7 @@ CreateConflictDetectionSlot(void) errmsg("creating replication conflict detection slot")); ReplicationSlotCreate(CONFLICT_DETECTION_SLOT, false, RS_PERSISTENT, false, - false, false); + false, false, false); init_conflict_slot_xmin(); } diff --git a/src/backend/replication/logical/logical.c b/src/backend/replication/logical/logical.c index 8ceaf64d164..d8e02c53558 100644 --- a/src/backend/replication/logical/logical.c +++ b/src/backend/replication/logical/logical.c @@ -108,9 +108,9 @@ static void LoadOutputPlugin(OutputPluginCallbacks *callbacks, const char *plugi * decoding. */ void -CheckLogicalDecodingRequirements(void) +CheckLogicalDecodingRequirements(bool repack) { - CheckSlotRequirements(); + CheckSlotRequirements(repack); /* * NB: Adding a new requirement likely means that RestoreSlotFromDisk() @@ -304,6 +304,7 @@ StartupDecodingContext(List *output_plugin_options, * output_plugin_options -- contains options passed to the output plugin * need_full_snapshot -- if true, must obtain a snapshot able to read all * tables; if false, one that can read only catalogs is acceptable. + * for_repack -- if true, we're going to be decoding for REPACK. * restart_lsn -- if given as invalid, it's this routine's responsibility to * mark WAL as reserved by setting a convenient restart_lsn for the slot. * Otherwise, we set for decoding to start from the given LSN without @@ -324,6 +325,7 @@ LogicalDecodingContext * CreateInitDecodingContext(const char *plugin, List *output_plugin_options, bool need_full_snapshot, + bool for_repack, XLogRecPtr restart_lsn, XLogReaderRoutine *xl_routine, LogicalOutputPluginWriterPrepareWrite prepare_write, @@ -340,7 +342,7 @@ CreateInitDecodingContext(const char *plugin, * On a standby, this check is also required while creating the slot. * Check the comments in the function. */ - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(for_repack); /* shorter lines... */ slot = MyReplicationSlot; diff --git a/src/backend/replication/logical/logicalfuncs.c b/src/backend/replication/logical/logicalfuncs.c index 9760818941d..512013b0ef0 100644 --- a/src/backend/replication/logical/logicalfuncs.c +++ b/src/backend/replication/logical/logicalfuncs.c @@ -115,7 +115,7 @@ pg_logical_slot_get_changes_guts(FunctionCallInfo fcinfo, bool confirm, bool bin CheckSlotPermissions(); - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(false); if (PG_ARGISNULL(0)) ereport(ERROR, diff --git a/src/backend/replication/logical/slotsync.c b/src/backend/replication/logical/slotsync.c index 8b53bd3ac7f..ae900f13467 100644 --- a/src/backend/replication/logical/slotsync.c +++ b/src/backend/replication/logical/slotsync.c @@ -434,7 +434,7 @@ get_local_synced_slots(void) LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (int i = 0; i < max_replication_slots; i++) + for (int i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; @@ -823,6 +823,7 @@ synchronize_one_slot(RemoteSlot *remote_slot, Oid remote_dbid, */ ReplicationSlotCreate(remote_slot->name, true, RS_TEMPORARY, remote_slot->two_phase, + false, remote_slot->failover, true); @@ -1707,7 +1708,7 @@ update_synced_slots_inactive_since(void) LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (int i = 0; i < max_replication_slots; i++) + for (int i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; diff --git a/src/backend/replication/slot.c b/src/backend/replication/slot.c index a1f37e59dbc..e6722fc0212 100644 --- a/src/backend/replication/slot.c +++ b/src/backend/replication/slot.c @@ -160,6 +160,8 @@ ReplicationSlot *MyReplicationSlot = NULL; /* GUC variables */ int max_replication_slots = 10; /* the maximum number of replication * slots */ +int max_repack_replication_slots = 5; /* the maximum number of slots + * for REPACK */ /* * Invalidate replication slots that have remained idle longer than this @@ -199,12 +201,13 @@ ReplicationSlotsShmemRequest(void *arg) { Size size; - if (max_replication_slots == 0) + if (max_replication_slots + max_repack_replication_slots == 0) return; size = offsetof(ReplicationSlotCtlData, replication_slots); size = add_size(size, - mul_size(max_replication_slots, sizeof(ReplicationSlot))); + mul_size(max_replication_slots + max_repack_replication_slots, + sizeof(ReplicationSlot))); ShmemRequestStruct(.name = "ReplicationSlot Ctl", .size = size, .ptr = (void **) &ReplicationSlotCtl, @@ -217,7 +220,7 @@ ReplicationSlotsShmemRequest(void *arg) static void ReplicationSlotsShmemInit(void *arg) { - for (int i = 0; i < max_replication_slots; i++) + for (int i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *slot = &ReplicationSlotCtl->replication_slots[i]; @@ -366,6 +369,7 @@ IsSlotForConflictCheck(const char *name) * db_specific: logical decoding is db specific; if the slot is going to * be used for that pass true, otherwise false. * two_phase: If enabled, allows decoding of prepared transactions. + * repack: If true, use a slot from the pool for REPACK. * failover: If enabled, allows the slot to be synced to standbys so * that logical replication can be resumed after failover. * synced: True if the slot is synchronized from the primary server. @@ -373,10 +377,11 @@ IsSlotForConflictCheck(const char *name) void ReplicationSlotCreate(const char *name, bool db_specific, ReplicationSlotPersistency persistency, - bool two_phase, bool failover, bool synced) + bool two_phase, bool repack, bool failover, bool synced) { ReplicationSlot *slot = NULL; - int i; + int startpoint, + endpoint; Assert(MyReplicationSlot == NULL); @@ -425,12 +430,16 @@ ReplicationSlotCreate(const char *name, bool db_specific, LWLockAcquire(ReplicationSlotAllocationLock, LW_EXCLUSIVE); /* - * Check for name collision, and identify an allocatable slot. We need to - * hold ReplicationSlotControlLock in shared mode for this, so that nobody - * else can change the in_use flags while we're looking at them. + * Check for name collision (across the whole array), and identify an + * allocatable slot (in the array slice specific to our current use case: + * either general, or REPACK only). We need to hold + * ReplicationSlotControlLock in shared mode for this, so that nobody else + * can change the in_use flags while we're looking at them. */ LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + startpoint = !repack ? 0 : max_replication_slots; + endpoint = max_replication_slots + (repack ? max_repack_replication_slots : 0); + for (int i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; @@ -438,7 +447,9 @@ ReplicationSlotCreate(const char *name, bool db_specific, ereport(ERROR, (errcode(ERRCODE_DUPLICATE_OBJECT), errmsg("replication slot \"%s\" already exists", name))); - if (!s->in_use && slot == NULL) + + if (i >= startpoint && i < endpoint && + !s->in_use && slot == NULL) slot = s; } LWLockRelease(ReplicationSlotControlLock); @@ -448,7 +459,8 @@ ReplicationSlotCreate(const char *name, bool db_specific, ereport(ERROR, (errcode(ERRCODE_CONFIGURATION_LIMIT_EXCEEDED), errmsg("all replication slots are in use"), - errhint("Free one or increase \"max_replication_slots\"."))); + errhint("Free one or increase \"%s\".", + repack ? "max_repack_replication_slots" : "max_replication_slots"))); /* * Since this slot is not in use, nobody should be looking at any part of @@ -541,7 +553,7 @@ SearchNamedReplicationSlot(const char *name, bool need_lock) if (need_lock) LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; @@ -569,7 +581,8 @@ int ReplicationSlotIndex(ReplicationSlot *slot) { Assert(slot >= ReplicationSlotCtl->replication_slots && - slot < ReplicationSlotCtl->replication_slots + max_replication_slots); + slot < ReplicationSlotCtl->replication_slots + + (max_replication_slots + max_repack_replication_slots)); return slot - ReplicationSlotCtl->replication_slots; } @@ -863,7 +876,7 @@ ReplicationSlotCleanup(bool synced_only) restart: found_valid_logicalslot = false; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; @@ -1245,7 +1258,7 @@ ReplicationSlotsComputeRequiredXmin(bool already_locked) if (!already_locked) LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; TransactionId effective_xmin; @@ -1300,7 +1313,7 @@ ReplicationSlotsComputeRequiredLSN(void) Assert(ReplicationSlotCtl != NULL); LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; XLogRecPtr restart_lsn; @@ -1367,12 +1380,12 @@ ReplicationSlotsComputeLogicalRestartLSN(void) XLogRecPtr result = InvalidXLogRecPtr; int i; - if (max_replication_slots <= 0) + if (max_replication_slots + max_repack_replication_slots <= 0) return InvalidXLogRecPtr; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s; XLogRecPtr restart_lsn; @@ -1447,11 +1460,11 @@ ReplicationSlotsCountDBSlots(Oid dboid, int *nslots, int *nactive) *nslots = *nactive = 0; - if (max_replication_slots <= 0) + if (max_replication_slots + max_repack_replication_slots <= 0) return false; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s; @@ -1508,13 +1521,13 @@ ReplicationSlotsDropDBSlots(Oid dboid) bool found_valid_logicalslot; bool dropped = false; - if (max_replication_slots <= 0) + if (max_replication_slots + max_repack_replication_slots <= 0) return; restart: found_valid_logicalslot = false; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s; char *slotname; @@ -1611,11 +1624,11 @@ CheckLogicalSlotExists(void) { bool found = false; - if (max_replication_slots <= 0) + if (max_replication_slots + max_repack_replication_slots <= 0) return false; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (int i = 0; i < max_replication_slots; i++) + for (int i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s; bool invalidated; @@ -1649,17 +1662,24 @@ CheckLogicalSlotExists(void) * slots. */ void -CheckSlotRequirements(void) +CheckSlotRequirements(bool repack) { /* * NB: Adding a new requirement likely means that RestoreSlotFromDisk() * needs the same check. */ - if (max_replication_slots == 0) + if (!repack && max_replication_slots == 0) ereport(ERROR, - (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("replication slots can only be used if \"max_replication_slots\" > 0"))); + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("replication slots can only be used if \"%s\" > 0", + "max_replication_slots")); + + if (repack && max_repack_replication_slots == 0) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("REPACK can only be used if \"%s\" > 0", + "max_repack_replication_slots")); if (wal_level < WAL_LEVEL_REPLICA) ereport(ERROR, @@ -2210,7 +2230,7 @@ InvalidateObsoleteReplicationSlots(uint32 possible_causes, Assert(!(possible_causes & RS_INVAL_WAL_REMOVED) || oldestSegno > 0); Assert(possible_causes != RS_INVAL_NONE); - if (max_replication_slots == 0) + if (max_replication_slots == 0 && max_repack_replication_slots == 0) return invalidated; XLogSegNoOffsetToRecPtr(oldestSegno, 0, wal_segment_size, oldestLSN); @@ -2218,7 +2238,7 @@ InvalidateObsoleteReplicationSlots(uint32 possible_causes, restart: found_valid_logicalslot = false; LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (int i = 0; i < max_replication_slots; i++) + for (int i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; bool released_lock = false; @@ -2323,7 +2343,7 @@ CheckPointReplicationSlots(bool is_shutdown) */ LWLockAcquire(ReplicationSlotAllocationLock, LW_SHARED); - for (i = 0; i < max_replication_slots; i++) + for (i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; char path[MAXPGPATH]; @@ -2424,7 +2444,7 @@ StartupReplicationSlots(void) FreeDir(replication_dir); /* currently no slots exist, we're done. */ - if (max_replication_slots <= 0) + if (max_replication_slots + max_repack_replication_slots <= 0) return; /* Now that we have recovered all the data, compute replication xmin */ diff --git a/src/backend/replication/slotfuncs.c b/src/backend/replication/slotfuncs.c index 9f5e4f998fe..16fbd383735 100644 --- a/src/backend/replication/slotfuncs.c +++ b/src/backend/replication/slotfuncs.c @@ -53,7 +53,7 @@ create_physical_replication_slot(char *name, bool immediately_reserve, /* acquire replication slot, this will check for conflicting names */ ReplicationSlotCreate(name, false, temporary ? RS_TEMPORARY : RS_PERSISTENT, false, - false, false); + false, false, false); if (immediately_reserve) { @@ -90,7 +90,7 @@ pg_create_physical_replication_slot(PG_FUNCTION_ARGS) CheckSlotPermissions(); - CheckSlotRequirements(); + CheckSlotRequirements(false); create_physical_replication_slot(NameStr(*name), immediately_reserve, @@ -146,7 +146,7 @@ create_logical_replication_slot(char *name, char *plugin, */ ReplicationSlotCreate(name, true, temporary ? RS_TEMPORARY : RS_EPHEMERAL, two_phase, - failover, false); + false, failover, false); /* * Ensure the logical decoding is enabled before initializing the logical @@ -164,6 +164,7 @@ create_logical_replication_slot(char *name, char *plugin, */ ctx = CreateInitDecodingContext(plugin, NIL, false, /* just catalogs is OK */ + false, /* not repack */ restart_lsn, XL_ROUTINE(.page_read = read_local_xlog_page, .segment_open = wal_segment_open, @@ -203,7 +204,7 @@ pg_create_logical_replication_slot(PG_FUNCTION_ARGS) CheckSlotPermissions(); - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(false); create_logical_replication_slot(NameStr(*name), NameStr(*plugin), @@ -240,7 +241,7 @@ pg_drop_replication_slot(PG_FUNCTION_ARGS) CheckSlotPermissions(); - CheckSlotRequirements(); + CheckSlotRequirements(false); ReplicationSlotDrop(NameStr(*name), true); @@ -270,7 +271,7 @@ pg_get_replication_slots(PG_FUNCTION_ARGS) currlsn = GetXLogWriteRecPtr(); LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); - for (slotno = 0; slotno < max_replication_slots; slotno++) + for (slotno = 0; slotno < max_replication_slots + max_repack_replication_slots; slotno++) { ReplicationSlot *slot = &ReplicationSlotCtl->replication_slots[slotno]; ReplicationSlot slot_contents; @@ -648,9 +649,9 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot) CheckSlotPermissions(); if (logical_slot) - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(false); else - CheckSlotRequirements(); + CheckSlotRequirements(false); LWLockAcquire(ReplicationSlotControlLock, LW_SHARED); @@ -665,7 +666,7 @@ copy_replication_slot(FunctionCallInfo fcinfo, bool logical_slot) * managed to create the new slot, we advance the new slot's restart_lsn * to the source slot's updated restart_lsn the second time we lock it. */ - for (int i = 0; i < max_replication_slots; i++) + for (int i = 0; i < max_replication_slots + max_repack_replication_slots; i++) { ReplicationSlot *s = &ReplicationSlotCtl->replication_slots[i]; diff --git a/src/backend/replication/walsender.c b/src/backend/replication/walsender.c index b4a2117a7f9..bad45adb004 100644 --- a/src/backend/replication/walsender.c +++ b/src/backend/replication/walsender.c @@ -1241,7 +1241,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd) { ReplicationSlotCreate(cmd->slotname, false, cmd->temporary ? RS_TEMPORARY : RS_PERSISTENT, - false, false, false); + false, false, false, false); if (reserve_wal) { @@ -1261,7 +1261,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd) Assert(cmd->kind == REPLICATION_KIND_LOGICAL); - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(false); /* * Initially create persistent slot as ephemeral - that allows us to @@ -1272,7 +1272,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd) */ ReplicationSlotCreate(cmd->slotname, true, cmd->temporary ? RS_TEMPORARY : RS_EPHEMERAL, - two_phase, failover, false); + two_phase, false, failover, false); /* * Do options check early so that we can bail before calling the @@ -1330,6 +1330,7 @@ CreateReplicationSlot(CreateReplicationSlotCmd *cmd) Assert(IsLogicalDecodingEnabled()); ctx = CreateInitDecodingContext(cmd->plugin, NIL, need_full_snapshot, + false, InvalidXLogRecPtr, XL_ROUTINE(.page_read = logical_read_xlog_page, .segment_open = WalSndSegmentOpen, @@ -1487,7 +1488,7 @@ StartLogicalReplication(StartReplicationCmd *cmd) QueryCompletion qc; /* make sure that our requirements are still fulfilled */ - CheckLogicalDecodingRequirements(); + CheckLogicalDecodingRequirements(false); Assert(!MyReplicationSlot); diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index fcb6ab80583..632f3ba4989 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -2079,6 +2079,14 @@ max => 'MAX_BACKENDS', }, +{ name => 'max_repack_replication_slots', type => 'int', context => 'PGC_POSTMASTER', group => 'REPLICATION_SENDING', + short_desc => 'Sets the maximum number of replication slots for use by REPACK.', + variable => 'max_repack_replication_slots', + boot_val => '5', + min => '0', + max => 'MAX_BACKENDS', +}, + /* see max_wal_senders */ { name => 'max_replication_slots', type => 'int', context => 'PGC_POSTMASTER', group => 'REPLICATION_SENDING', short_desc => 'Sets the maximum number of simultaneously defined replication slots.', diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index e3e462f3efb..2e10eb4a36a 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -348,6 +348,8 @@ # (change requires restart) #max_replication_slots = 10 # max number of replication slots # (change requires restart) +#max_repack_replication_slots = 5 # max number of replication slots for REPACK + # (change requires restart) #wal_keep_size = 0 # in megabytes; 0 disables #max_slot_wal_keep_size = -1 # in megabytes; -1 disables #idle_replication_slot_timeout = 0 # in seconds; 0 disables diff --git a/src/include/replication/logical.h b/src/include/replication/logical.h index bc9d4ece672..bc075b16741 100644 --- a/src/include/replication/logical.h +++ b/src/include/replication/logical.h @@ -115,11 +115,12 @@ typedef struct LogicalDecodingContext } LogicalDecodingContext; -extern void CheckLogicalDecodingRequirements(void); +extern void CheckLogicalDecodingRequirements(bool repack); extern LogicalDecodingContext *CreateInitDecodingContext(const char *plugin, List *output_plugin_options, bool need_full_snapshot, + bool for_repack, XLogRecPtr restart_lsn, XLogReaderRoutine *xl_routine, LogicalOutputPluginWriterPrepareWrite prepare_write, diff --git a/src/include/replication/slot.h b/src/include/replication/slot.h index 1a3557de607..77c8d0975b6 100644 --- a/src/include/replication/slot.h +++ b/src/include/replication/slot.h @@ -324,13 +324,14 @@ extern PGDLLIMPORT ReplicationSlot *MyReplicationSlot; /* GUCs */ extern PGDLLIMPORT int max_replication_slots; +extern PGDLLIMPORT int max_repack_replication_slots; extern PGDLLIMPORT char *synchronized_standby_slots; extern PGDLLIMPORT int idle_replication_slot_timeout_secs; /* management of individual slots */ extern void ReplicationSlotCreate(const char *name, bool db_specific, ReplicationSlotPersistency persistency, - bool two_phase, bool failover, + bool two_phase, bool repack, bool failover, bool synced); extern void ReplicationSlotPersist(void); extern void ReplicationSlotDrop(const char *name, bool nowait); @@ -373,7 +374,7 @@ extern void ReplicationSlotDropAtPubNode(WalReceiverConn *wrconn, char *slotname extern void StartupReplicationSlots(void); extern void CheckPointReplicationSlots(bool is_shutdown); -extern void CheckSlotRequirements(void); +extern void CheckSlotRequirements(bool repack); extern void CheckSlotPermissions(void); extern ReplicationSlotInvalidationCause GetSlotInvalidationCause(const char *cause_name); -- 2.47.3 --qs77q6fpwolne4ds Content-Type: text/x-diff; charset=utf-8 Content-Disposition: attachment; filename="v56-0003-Error-out-any-process-that-would-block-at-REPACK.patch" ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
* [PATCH] Lock upgrade without deadlocks. @ 2026-04-17 13:13 Antonin Houska <[email protected]> 0 siblings, 0 replies; 97+ messages in thread From: Antonin Houska @ 2026-04-17 13:13 UTC (permalink / raw) For REPACK (CONCURRENTLY), it's essential to upgrade ShareUpdateExclusive lock to AccessExclusiveLock for the final stage of table processing. However, we cannot release the earlier before requesting the latter, because if another process ran DDL on the table in between, REPACK would have to cancel its transaction, and waste possibly a lot of work. Without releasing ShareUpdateExclusive lock, the upgrade can cause deadlock, and it's not really deterministic whether REPACK or the other process will get canceled. This patch adds a new field to the LOCK structure which tells which process is trying to upgrade the lock - this field is set right before the upgrade. All processes waiting in the lock queue are then woken up to check the flag. When another process tries to get the lock after that, it checks this field, and if it's set, it checks for deadlocks even if deadlock timeout hasn't expired yet. If the process is already in the lock's queue and sleeping, the lock upgrading process wakes it up so it checks the flag immediately. At the time the upgrading process starts to sleep, all the other process should be aware that they need to check for deadlock during their lock acquisitions, so the upgrading process does not have to check for deadlocks when it gets woken up. Thus it should never receive deadlock error report. --- src/backend/commands/repack.c | 4 +- src/backend/storage/lmgr/lmgr.c | 35 ++++++++--- src/backend/storage/lmgr/lock.c | 107 +++++++++++++++++++++++++++++++- src/backend/storage/lmgr/proc.c | 32 +++++++--- src/include/storage/lmgr.h | 1 + src/include/storage/lock.h | 5 +- src/include/storage/proc.h | 4 +- 7 files changed, 166 insertions(+), 22 deletions(-) diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 58e3867246f..310b2a65099 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -3055,8 +3055,10 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, /* * Acquire AccessExclusiveLock on the table, its TOAST relation (if there * is one), all its indexes, so that we can swap the files. + * + * TODO The same for indexes and TOAST? */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + LockRelationOidUpgrade(old_table_oid, AccessExclusiveLock); /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/storage/lmgr/lmgr.c b/src/backend/storage/lmgr/lmgr.c index 2ccf7237fee..0b103485d54 100644 --- a/src/backend/storage/lmgr/lmgr.c +++ b/src/backend/storage/lmgr/lmgr.c @@ -58,6 +58,8 @@ typedef struct XactLockTableWaitInfo const ItemPointerData *ctid; } XactLockTableWaitInfo; +static void LockRelationOidCommon(Oid relid, LOCKMODE lockmode, + bool isUpgrade); static void XactLockTableWaitErrorCb(void *arg); /* @@ -105,6 +107,21 @@ SetLocktagRelationOid(LOCKTAG *tag, Oid relid) */ void LockRelationOid(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, false); +} + +/* + * Like above, but upgrade an existing lock. + */ +void +LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode) +{ + LockRelationOidCommon(relid, lockmode, true); +} + +static void +LockRelationOidCommon(Oid relid, LOCKMODE lockmode, bool isUpgrade) { LOCKTAG tag; LOCALLOCK *locallock; @@ -113,7 +130,7 @@ LockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, isUpgrade); /* * Now that we have the lock, check for invalidation messages, so that we @@ -157,7 +174,7 @@ ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode) SetLocktagRelationOid(&tag, relid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -191,7 +208,7 @@ LockRelationId(LockRelId *relid, LOCKMODE lockmode) SET_LOCKTAG_RELATION(tag, relid->dbId, relid->relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -254,7 +271,7 @@ LockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, false, true, &locallock, - false); + false, false); /* * Now that we have the lock, check for invalidation messages; see notes @@ -286,7 +303,7 @@ ConditionalLockRelation(Relation relation, LOCKMODE lockmode) relation->rd_lockInfo.lockRelId.relId); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -591,7 +608,7 @@ ConditionalLockTuple(Relation relation, const ItemPointerData *tid, LOCKMODE loc ItemPointerGetOffsetNumber(tid)); return (LockAcquireExtended(&tag, lockmode, false, true, true, NULL, - logLockFailure) != LOCKACQUIRE_NOT_AVAIL); + logLockFailure, false) != LOCKACQUIRE_NOT_AVAIL); } /* @@ -749,7 +766,7 @@ ConditionalXactLockTableWait(TransactionId xid, bool logLockFailure) SET_LOCKTAG_TRANSACTION(tag, xid); if (LockAcquireExtended(&tag, ShareLock, false, true, true, NULL, - logLockFailure) + logLockFailure, false) == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1043,7 +1060,7 @@ ConditionalLockDatabaseObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; @@ -1123,7 +1140,7 @@ ConditionalLockSharedObject(Oid classid, Oid objid, uint16 objsubid, objsubid); res = LockAcquireExtended(&tag, lockmode, false, true, true, &locallock, - false); + false, false); if (res == LOCKACQUIRE_NOT_AVAIL) return false; diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c index c221fe96889..bcbbf36c091 100644 --- a/src/backend/storage/lmgr/lock.c +++ b/src/backend/storage/lmgr/lock.c @@ -810,7 +810,7 @@ LockAcquire(const LOCKTAG *locktag, bool dontWait) { return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait, - true, NULL, false); + true, NULL, false, false); } /* @@ -829,6 +829,9 @@ LockAcquire(const LOCKTAG *locktag, * * logLockFailure indicates whether to log details when a lock acquisition * fails with dontWait = true. + * + * isUpgrade should be true if the backend already holds this lock in a lower + * mode. */ LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, @@ -837,7 +840,8 @@ LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure) + bool logLockFailure, + bool isUpgrade) { LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid; LockMethod lockMethodTable; @@ -1119,7 +1123,8 @@ LockAcquireExtended(const LOCKTAG *locktag, * case, because JoinWaitQueue() may discover that we can acquire the * lock immediately after all. */ - waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait); + waitResult = JoinWaitQueue(locallock, lockMethodTable, dontWait, + isUpgrade); } if (waitResult == PROC_WAIT_STATUS_ERROR) @@ -1225,8 +1230,85 @@ LockAcquireExtended(const LOCKTAG *locktag, Assert(!dontWait); PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock); LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode); + + /* + * Lock upgrade can introduce deadlock, therefore enforce special + * behavior of other processes that deal with this lock. In + * particular, any waiter that sees upgradedBy set is expected to + * perform deadlock check as soon as it's woken up. (Since we are + * already in the queue, the deadlock detector has all the information + * it needs.) + */ + if (isUpgrade) + { + dlist_iter iter; + + /* + * There should not be multiple upgrades at the same time. XXX + * ERROR ? + */ + Assert(lock->upgradedBy == NULL); + + lock->upgradedBy = MyProc; + + /* + * Now, before I sleep myself, wake up all the existing waiters + * (except for me), so they check for deadlock. + */ + dclist_foreach(iter, &lock->waitProcs) + { + PGPROC *proc = dlist_container(PGPROC, waitLink, + iter.cur); + + if (proc != MyProc) + SetLatch(&proc->procLatch); + } + } LWLockRelease(partitionLock); + if (!isUpgrade) + { + HASH_SEQ_STATUS status; + LOCALLOCK *llock; + bool check_deadlock = false; + + /* + * Likewise, new waiters should check the isUpgrade flag and + * engage deadlock detector if needed. The lock being acquired + * should already be in the hash. + * + * Note that, to make deadlock detection happen as soon as + * possible (i.e. regardless deadlock timeout), we check the other + * locks too. XXX There might be ways to get into the deadlock + * indirectly, but that needs more analysis. As long as the + * upgrading process skips deadlock detection altogether, the + * worst consequence of missing some lock wait cycle here is that + * the other process will be kicked-off no sooner than after its + * deadlock timeout has elapsed. (Which in turn means that the + * lock upgrade will take longer than expected.) + */ + hash_seq_init(&status, LockMethodLocalHash); + while ((llock = (LOCALLOCK *) hash_seq_search(&status)) != NULL) + { + LOCK *mylock = llock->lock; + + if (mylock && mylock->upgradedBy) + { + check_deadlock = true; + break; + } + } + /* Check for deadlock if needed. */ + if (check_deadlock) + { + DeadLockState deadlock_state; + + deadlock_state = CheckDeadLock(); + if (deadlock_state == DS_HARD_DEADLOCK) + DeadLockReport(); + } + } + waitResult = WaitOnLock(locallock, owner); /* @@ -1245,6 +1327,18 @@ LockAcquireExtended(const LOCKTAG *locktag, DeadLockReport(); /* DeadLockReport() will not return */ } + + /* + * If finishing the lock upgrade, we should not get into a deadlock + * anymore, so let others know that they do not have to care either. + */ + if (isUpgrade) + { + LWLockAcquire(partitionLock, LW_EXCLUSIVE); + Assert(lock->upgradedBy != NULL); + lock->upgradedBy = NULL; + LWLockRelease(partitionLock); + } } else LWLockRelease(partitionLock); @@ -1322,6 +1416,7 @@ SetupLockInTable(LockMethod lockMethodTable, PGPROC *proc, lock->nGranted = 0; MemSet(lock->requested, 0, sizeof(int) * MAX_LOCKMODES); MemSet(lock->granted, 0, sizeof(int) * MAX_LOCKMODES); + lock->upgradedBy = NULL; LOCK_PRINT("LockAcquire: new", lock, lockmode); } else @@ -1768,6 +1863,12 @@ CleanUpLock(LOCK *lock, PROCLOCK *proclock, elog(PANIC, "proclock table corrupted"); } + /* + * Was this backend upgrading the lock? + */ + if (lock->upgradedBy == MyProc) + lock->upgradedBy = NULL; + if (lock->nRequested == 0) { /* diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1ac25068d62..7b31370db70 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -95,7 +95,6 @@ static volatile sig_atomic_t got_deadlock_timeout; static void RemoveProcFromArray(int code, Datum arg); static void ProcKill(int code, Datum arg); static void AuxiliaryProcKill(int code, Datum arg); -static DeadLockState CheckDeadLock(void); /* @@ -1143,7 +1142,8 @@ AuxiliaryPidGetProc(int pid) * NOTES: The process queue is now a priority queue for locking. */ ProcWaitStatus -JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) +JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait, + bool isUpgrade) { LOCKMODE lockmode = locallock->tag.mode; LOCK *lock = locallock->lock; @@ -1230,8 +1230,12 @@ JoinWaitQueue(LOCALLOCK *locallock, LockMethod lockMethodTable, bool dontWait) /* Must he wait for me? */ if (lockMethodTable->conflictTab[proc->waitLockMode] & myHeldLocks) { - /* Must I wait for him ? */ - if (lockMethodTable->conflictTab[lockmode] & proc->heldLocks) + /* + * Must I wait for him? I don't want a deadlock during lock + * upgrade - other processes should fail on it. + */ + if ((lockMethodTable->conflictTab[lockmode] & proc->heldLocks) && + !isUpgrade) { /* * Yes, so we have a deadlock. Easiest way to clean up @@ -1461,8 +1465,22 @@ ProcSleep(LOCALLOCK *locallock) (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_EXIT_ON_PM_DEATH, 0, PG_WAIT_LOCK | locallock->tag.lock.locktag_type); ResetLatch(MyLatch); - /* check for deadlocks first, as that's probably log-worthy */ - if (got_deadlock_timeout) + /* + * Check for deadlocks first, as that's probably log-worthy. + * + * Do not wait for the timeout if the lock is being upgraded since + * the risk of deadlock is higher now. However, do not check for + * deadlock if the lock is being upgraded by this process - other + * processes should take care. + * + * TODO Possible optimization: if this is the only lock of the + * backend and if it did not have any weaker lock on the table so + * far, it should be safe to skip the deadlock check. However, to + * evaluate the situation, we need to take fast-path locks into + * account. + */ + if ((got_deadlock_timeout || lock->upgradedBy) && + lock->upgradedBy != MyProc) { deadlock_state = CheckDeadLock(); got_deadlock_timeout = false; @@ -1819,7 +1837,7 @@ ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock) * not, just return. If we have a real deadlock, remove ourselves from the * lock's wait queue. */ -static DeadLockState +DeadLockState CheckDeadLock(void) { int i; diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h index 2a985ce5e15..eddbe1e8622 100644 --- a/src/include/storage/lmgr.h +++ b/src/include/storage/lmgr.h @@ -38,6 +38,7 @@ extern void RelationInitLockInfo(Relation relation); /* Lock a relation */ extern void LockRelationOid(Oid relid, LOCKMODE lockmode); +extern void LockRelationOidUpgrade(Oid relid, LOCKMODE lockmode); extern void LockRelationId(LockRelId *relid, LOCKMODE lockmode); extern bool ConditionalLockRelationOid(Oid relid, LOCKMODE lockmode); extern void UnlockRelationId(LockRelId *relid, LOCKMODE lockmode); diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h index ee3cb1dc203..080b69eddfd 100644 --- a/src/include/storage/lock.h +++ b/src/include/storage/lock.h @@ -131,6 +131,7 @@ typedef const LockMethodData *LockMethod; * nRequested -- total requested locks of all types. * granted -- count of each lock type currently granted on the lock. * nGranted -- total granted locks of all types. + * upgrader -- process that intends to upgrade the lock * * Note: these counts count 1 for each backend. Internally to a backend, * there may be multiple grabs on a particular lock, but this is not reflected @@ -150,6 +151,7 @@ typedef struct LOCK int nRequested; /* total of requested[] array */ int granted[MAX_LOCKMODES]; /* counts of granted locks */ int nGranted; /* total of granted[] array */ + PGPROC *upgradedBy; /* is lock being upgraded by this process? */ } LOCK; #define LOCK_LOCKMETHOD(lock) ((LOCKMETHODID) (lock).tag.locktag_lockmethodid) @@ -390,7 +392,8 @@ extern LockAcquireResult LockAcquireExtended(const LOCKTAG *locktag, bool dontWait, bool reportMemoryError, LOCALLOCK **locallockp, - bool logLockFailure); + bool logLockFailure, + bool isUpgrade); extern void AbortStrongLockAcquire(void); extern void MarkLockClear(LOCALLOCK *locallock); extern bool LockRelease(const LOCKTAG *locktag, diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h index 3e1d1fad5f9..c4bc3a24044 100644 --- a/src/include/storage/proc.h +++ b/src/include/storage/proc.h @@ -563,10 +563,12 @@ extern bool HaveNFreeProcs(int n, int *nfree); extern void ProcReleaseLocks(bool isCommit); extern ProcWaitStatus JoinWaitQueue(LOCALLOCK *locallock, - LockMethod lockMethodTable, bool dontWait); + LockMethod lockMethodTable, bool dontWait, + bool isUpgrade); extern ProcWaitStatus ProcSleep(LOCALLOCK *locallock); extern void ProcWakeup(PGPROC *proc, ProcWaitStatus waitStatus); extern void ProcLockWakeup(LockMethod lockMethodTable, LOCK *lock); +extern DeadLockState CheckDeadLock(void); extern void CheckDeadLockAlert(void); extern void LockErrorCleanup(void); extern void GetLockHoldersAndWaiters(LOCALLOCK *locallock, -- 2.47.3 --=-=-=-- ^ permalink raw reply [nested|flat] 97+ messages in thread
end of thread, other threads:[~2026-04-17 13:13 UTC | newest] Thread overview: 97+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2026-04-01 17:54 [PATCH v56 2/3] Reserve replication slots specifically for REPACK Álvaro Herrera <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]> 2026-04-17 13:13 [PATCH] Lock upgrade without deadlocks. Antonin Houska <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox