From b38a428c77da8b188e765bf78a546c5d2304907b Mon Sep 17 00:00:00 2001 From: Shihao Date: Tue, 29 Sep 2026 22:55:42 -0600 Subject: [PATCH v1] Do not let REPACK (CONCURRENTLY) block the table while waiting for the final lock While REPACK queues for AccessExclusiveLock, all new lock requests on the table queue behind it. Poll for the lock instead, and apply the changes made meanwhile between attempts. lock_timeout limits how long we keep polling. --- doc/src/sgml/ref/repack.sgml | 15 ++++-- src/backend/commands/repack.c | 47 ++++++++++++++++++- .../utils/activity/wait_event_names.txt | 1 + 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/doc/src/sgml/ref/repack.sgml b/doc/src/sgml/ref/repack.sgml index 346cba89c90..e06045326fe 100644 --- a/doc/src/sgml/ref/repack.sgml +++ b/doc/src/sgml/ref/repack.sgml @@ -231,11 +231,16 @@ REPACK [ ( option [, ...] ) ] USING () and applied before the ACCESS EXCLUSIVE lock is requested. Thus the lock is typically held only for the time needed to swap the files, which - should be pretty short. However, the time might still be noticeable if - too many data changes have been done to the table while - REPACK was waiting for the lock: those changes must - be processed just before the files are swapped, while the - ACCESS EXCLUSIVE lock is being held. + should be pretty short. + + + + REPACK does not queue for the ACCESS + EXCLUSIVE lock, because other transactions that want to use + the table would have to wait behind it. Instead, it checks whether the + lock is available, and while it is not, it applies the data changes that + take place meanwhile and checks again. , + if set, limits how long it keeps trying. diff --git a/src/backend/commands/repack.c b/src/backend/commands/repack.c index 596c1abaf78..d7e632e1750 100644 --- a/src/backend/commands/repack.c +++ b/src/backend/commands/repack.c @@ -82,6 +82,7 @@ #include "utils/relmapper.h" #include "utils/snapmgr.h" #include "utils/syscache.h" +#include "utils/timestamp.h" #include "utils/wait_event_types.h" /* @@ -101,6 +102,9 @@ typedef struct */ #define WORKER_FILE_SNAPSHOT 0 +/* How often to check for the final AccessExclusiveLock, in milliseconds. */ +#define REPACK_LOCK_WAIT_INTERVAL 50 + /* * Information needed to apply concurrent data changes. */ @@ -3388,6 +3392,7 @@ rebuild_relation_finish_concurrent(Relation NewHeap, Relation OldHeap, XLogRecPtr end_of_wal; List *indexrels; ChangeContext chgcxt; + TimestampTz lock_wait_start; Assert(CheckRelationLockedByMe(OldHeap, ShareUpdateExclusiveLock, false)); Assert(CheckRelationLockedByMe(NewHeap, AccessExclusiveLock, false)); @@ -3458,8 +3463,48 @@ 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. + * + * Do not queue for the table lock. While we are queued, every new lock + * request on the table queues behind us, so waiting behind one + * long-running transaction would block all access to the table for that + * long. Instead, keep checking whether the lock is available, and apply + * the changes that arrive in the meantime, so that the work left for the + * time we hold the lock stays small. lock_timeout limits how long we + * keep trying. */ - LockRelationOid(old_table_oid, AccessExclusiveLock); + lock_wait_start = GetCurrentTimestamp(); + while (!ConditionalLockRelationOid(old_table_oid, AccessExclusiveLock)) + { + CHECK_FOR_INTERRUPTS(); + + if (LockTimeout > 0 && + TimestampDifferenceExceeds(lock_wait_start, GetCurrentTimestamp(), + LockTimeout)) + ereport(ERROR, + errcode(ERRCODE_LOCK_NOT_AVAILABLE), + errmsg("canceling statement due to lock timeout")); + + XLogFlush(GetXLogInsertEndRecPtr()); + end_of_wal = GetFlushRecPtr(NULL); + process_concurrent_changes(end_of_wal, &chgcxt, false); + + /* + * If someone is already waiting behind the lock we hold, they may + * hold a lock on the table themselves, which they will never release + * while waiting for us. Wait for the lock the normal way then, so + * that we either get it ahead of them or the deadlock detector + * resolves the situation. + */ + if (LockHasWaitersRelation(OldHeap, ShareUpdateExclusiveLock)) + { + LockRelationOid(old_table_oid, AccessExclusiveLock); + break; + } + + (void) WaitLatch(MyLatch, WL_LATCH_SET | WL_TIMEOUT | WL_EXIT_ON_PM_DEATH, + REPACK_LOCK_WAIT_INTERVAL, WAIT_EVENT_REPACK_LOCK_WAIT); + ResetLatch(MyLatch); + } /* * Lock all indexes now, not only the clustering one: all indexes need to diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 3d366fd1114..94f9bdafc0b 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -153,6 +153,7 @@ RECOVERY_CONFLICT_SNAPSHOT "Waiting for recovery conflict resolution for a vacuu RECOVERY_CONFLICT_TABLESPACE "Waiting for recovery conflict resolution for dropping a tablespace." RECOVERY_END_COMMAND "Waiting for to complete." RECOVERY_PAUSE "Waiting for recovery to be resumed." +REPACK_LOCK_WAIT "Waiting to acquire an exclusive lock on a table repacked concurrently, to swap its files." REPACK_WORKER_EXPORT "Waiting for decoding worker to export a new output file." REPLICATION_ORIGIN_DROP "Waiting for a replication origin to become inactive so it can be dropped." REPLICATION_SLOT_DROP "Waiting for a replication slot to become inactive so it can be dropped." -- 2.37.1 (Apple Git-137.1)