REPACK (CONCURRENTLY): do not block the table while waiting for the final lock

Started by shihao zhong23 days ago4 messageshackers
Beta feature

Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.

appliessuccessCI history

You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:

docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t253998
psql -h localhost -U postgres

Built from patchset v2 (message #2), October 22, 2026 at 04:24 AM.

Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:

git clone --branch t253998_2 https://github.com/hackorum-dev/postgres.git

In a checkout you already have, add the fork once:

git remote add hackorum https://github.com/hackorum-dev/postgres.git

then, for this patchset and every later one:

git fetch hackorum t253998_2 && git checkout t253998_2

Patchset v2 (message #2) is on t253998_2

Jump to latest
#1shihao zhong
zhong950419@gmail.com

Subject: REPACK (CONCURRENTLY): do not block the table while waiting for
the final lock

Hi hackers,

REPACK (CONCURRENTLY) requests AccessExclusiveLock at the end to swap
the files. While it is queued for that lock behind a long running
transaction, every new query on the table queues behind it. On master I
held AccessShareLock in one session for 15 seconds, and a single row
SELECT that arrived while REPACK was waiting took 14.7 seconds. The
usual defense, lock_timeout, makes it worse here. With lock_timeout = 3s
the REPACK fails after all the copying is done.

The attached patch makes REPACK stop queueing for that lock. It checks
whether the lock is available. While it is not, it applies the changes
that arrived meanwhile and checks again every 50 ms, the way
lazy_truncate_heap() does. With the patch the same SELECT took 0.9
seconds, and REPACK finished once the long transaction ended.
lock_timeout limits how long it keeps trying, so its meaning does not
change.

The cost is on the REPACK side. A conditional request only succeeds when
nobody holds a lock at that moment, so on a busy table it can take a
while. With 16 pgbench clients doing single row SELECTs on the table
(about 135k tps), REPACK needed 1 to 10 seconds to get the lock, against
0.5 seconds when queueing. After hours of copying I think that is fine.

There is a variant with a lock manager change that queues for
deadlock_timeout at a time and then leaves the queue. It gets the lock
in 0.5 seconds on that busy table, but it is 130 lines in lock.c and
proc.c for one caller. I can post it if there is interest.

One case needs the old behavior. If a session already waits behind the
lock REPACK holds, it may hold a lock on the table itself, and polling
would never end. So when LockHasWaitersRelation() reports a waiter,
REPACK waits for the lock the normal way. A real deadlock is then
reported as before, with REPACK as the victim.

Thanks,
Shihao

Attachments:

t253998_1
v1-0001-Do-not-let-REPACK-CONCURRENTLY-block-the-table-wh.patchapplication/octet-stream; name=v1-0001-Do-not-let-REPACK-CONCURRENTLY-block-the-table-wh.patchDownload+57-7
#2Antonin Houska
ah@cybertec.at
In reply to: shihao zhong (#1)
Re: REPACK (CONCURRENTLY): do not block the table while waiting for the final lock

shihao zhong <zhong950419@gmail.com> wrote:

Subject: REPACK (CONCURRENTLY): do not block the table while waiting for the final lock

[ I assume this is not meant for v19, is it? ]

REPACK (CONCURRENTLY) requests AccessExclusiveLock at the end to swap
the files. While it is queued for that lock behind a long running
transaction, every new query on the table queues behind it. On master I
held AccessShareLock in one session for 15 seconds, and a single row
SELECT that arrived while REPACK was waiting took 14.7 seconds. The
usual defense, lock_timeout, makes it worse here. With lock_timeout = 3s
the REPACK fails after all the copying is done.

The attached patch makes REPACK stop queueing for that lock. It checks
whether the lock is available. While it is not, it applies the changes
that arrived meanwhile and checks again every 50 ms, the way
lazy_truncate_heap() does. With the patch the same SELECT took 0.9
seconds, and REPACK finished once the long transaction ended.
lock_timeout limits how long it keeps trying, so its meaning does not
change.

The cost is on the REPACK side. A conditional request only succeeds when
nobody holds a lock at that moment, so on a busy table it can take a
while. With 16 pgbench clients doing single row SELECTs on the table
(about 135k tps), REPACK needed 1 to 10 seconds to get the lock, against
0.5 seconds when queueing. After hours of copying I think that is fine.

There is a variant with a lock manager change that queues for
deadlock_timeout at a time and then leaves the queue.

Waiting for a limited time would make more sense, but why exactly
deadlock_timeout should control that?

Attached is my proposal to restrict the wait time, w/o hacking the lock
manager. Note that it deliberately does not teach REPACK to give up. Unlike
(lazy) VACUUM, all the work is rolled back if REPACK ends with ERROR.

--
Antonin Houska
Web: https://www.cybertec-postgresql.com

Attachments:

t253998_2
0001-Limit-the-time-for-REPACK-CONCURRENTLY-to-stay-in-th.patchtext/x-diffDownload+71-2
#3Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Antonin Houska (#2)
Re: REPACK (CONCURRENTLY): do not block the table while waiting for the final lock

On 2026-Oct-06, Antonin Houska wrote:

shihao zhong <zhong950419@gmail.com> wrote:

Subject: REPACK (CONCURRENTLY): do not block the table while waiting for the final lock

[ I assume this is not meant for v19, is it? ]

I definitely dont't want to change this behavior in 19. At the most,
lets document any shortcomings that the current behavior may have.

For that reason, I don't have any intentions on studying any of these
ideas until we're out of the woods for 19, so to speak :-) -- and out of
2026 TBH.

--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/

#4shihao zhong
zhong950419@gmail.com
In reply to: Alvaro Herrera (#3)
Re: REPACK (CONCURRENTLY): do not block the table while waiting for the final lock

Hi,

[ I assume this is not meant for v19, is it? ]

I definitely dont't want to change this behavior in 19. At the most,
lets document any shortcomings that the current behavior may have.

Right, this is for 20, not 19. I will leave 19 alone and come back to
it once 19 is out. .

Antonin, thanks for your proposal. I will review your patch little later.

Thanks,
Shihao